Compare commits

...
26 changed files with 992 additions and 195 deletions

No files matched your search

+12
View File
@@ -2,6 +2,18 @@
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
## v0.8.4-beta
**New Features**:
- Remember folder view type (and sorting) for each folder #966
- heic is supported in preview and viewer (via conversion) when on non-safari browser #1191
**Notes**:
**BugFixes**:
- Access Management: issue with access settings #1195
- fix shutdown panic related to sse connection
## v0.8.4-beta
**New Features**:
+3 -3
View File
@@ -9,14 +9,14 @@ Thank you for your interest in contributing to FileBrowser Quantum! This guide w
- **Docker** (optional, for containerized development)
- **Git**
### Required Tools
### Optional Tools
- **ffmpeg**: Required for video thumbnail generation
- **ffmpeg**: For media features (subtitles, thumbnails, heic)
- Ubuntu/Debian: `sudo apt-get install ffmpeg`
- macOS: `brew install ffmpeg`
- Windows: Download from [ffmpeg.org](https://ffmpeg.org/download.html)
- **mupdf-tools**: Required for PDF preview generation
- **mupdf-tools**: For PDF preview generation
- Ubuntu/Debian: `sudo apt-get install mupdf-tools`
- macOS: `brew install mupdf-tools`
- Windows: Download from [mupdf.com](https://mupdf.com/downloads/)
+71 -13
View File
@@ -273,32 +273,91 @@ func (s *Storage) Permitted(sourcePath, indexPath, username string) bool {
}
func (s *Storage) computePermitted(sourcePath, indexPath, username string) bool {
var rulesFound []*AccessRule
// Walk up the path hierarchy and collect all relevant rules
currentPath := indexPath
for {
permitted, found := s.permittedAtExactPath(sourcePath, indexPath, username)
rule, found := s.getRuleAtExactPath(sourcePath, currentPath)
if found {
return permitted
rulesFound = append(rulesFound, rule)
}
indexPath = utils.GetParentDirectoryPath(indexPath)
if indexPath == "" {
if currentPath == "/" || currentPath == "." || currentPath == "" {
break
}
currentPath = utils.GetParentDirectoryPath(currentPath)
}
// Now evaluate the rules, starting from the most specific (indexPath) to the least specific (root)
for _, rule := range rulesFound {
permitted, hasSpecificRule := s.evaluateRuleForUser(rule, username)
if hasSpecificRule {
return permitted
}
}
// No rules found anywhere in the path hierarchy
// Check if the source has DenyByDefault configured (acts like a root-level denyAll rule)
// No specific user or group rule found in the hierarchy.
// Check for any DenyAll rule in the path.
for _, rule := range rulesFound {
if rule.DenyAll {
return false
}
}
// No specific rules found anywhere in the path hierarchy.
// Fallback to the source's DenyByDefault setting.
sourceInfo, ok := settings.Config.Server.SourceMap[sourcePath]
if !ok {
logger.Errorf("source %s not found in config during access check", sourcePath)
return false
}
if sourceInfo.Config.DenyByDefault {
return false
}
return true
return !sourceInfo.Config.DenyByDefault
}
// getRuleAtExactPath is a helper to get a rule without the recursive logic.
func (s *Storage) getRuleAtExactPath(sourcePath, indexPath string) (*AccessRule, bool) {
s.mux.RLock()
defer s.mux.RUnlock()
rulesBySource, ok := s.AllRules[sourcePath]
if !ok {
return nil, false
}
rule, ok := rulesBySource[indexPath]
return rule, ok
}
// evaluateRuleForUser evaluates a single rule for a user and returns if a specific rule was found.
func (s *Storage) evaluateRuleForUser(rule *AccessRule, username string) (permitted bool, hasSpecificRule bool) {
// Check user deny first
if _, found := rule.Deny.Users[username]; found {
return false, true
}
// Check group deny
for group := range rule.Deny.Groups {
if s.isUserInGroup(username, group) {
return false, true
}
}
// Check user allow
if _, found := rule.Allow.Users[username]; found {
return true, true
}
// Check group allow
for group := range rule.Allow.Groups {
if s.isUserInGroup(username, group) {
return true, true
}
}
// No specific rule for this user in this rule set.
return false, false
}
/*
// permittedAtExactPath checks if a rule exists at the given path and evaluates it if so.
func (s *Storage) permittedAtExactPath(sourcePath, indexPath, username string) (bool, bool) {
s.mux.RLock()
@@ -371,6 +430,7 @@ func (s *Storage) permittedRule(rule *AccessRule, username string, sourcePath st
return true
}
*/
// isUserInGroup checks if a username is in a group.
func (s *Storage) isUserInGroup(username, group string) bool {
@@ -421,8 +481,6 @@ func (s *Storage) GetFrontendRules(sourcePath, indexPath string) (FrontendAccess
frontendRules.Deny.Groups = utils.NonNilSlice(slices.Collect(maps.Keys(rule.Deny.Groups)))
frontendRules.Allow.Users = utils.NonNilSlice(slices.Collect(maps.Keys(rule.Allow.Users)))
frontendRules.Allow.Groups = utils.NonNilSlice(slices.Collect(maps.Keys(rule.Allow.Groups)))
logger.Debugf("Returning frontend rules for source: %s and index: %s", sourcePath, indexPath)
return frontendRules, ok
}
+74
View File
@@ -405,3 +405,77 @@ func TestPermitted_DenyByDefaultWithDenyAll(t *testing.T) {
t.Error("bob should not be permitted (DenyByDefault is true and no allow rule)")
}
}
func TestPermitted_DenyByDefault_AdminRootAccess(t *testing.T) {
// 1. Setup
access.ClearCache()
s, userStore := createTestStorage(t)
createTestUser(t, userStore, "admin")
createTestUser(t, userStore, "graham")
createTestUser(t, userStore, "bob")
err := s.LoadFromDB()
if err != nil && err != storm.ErrNotFound {
t.Errorf("unexpected error loading from DB: %v", err)
}
// 2. Configure source with DenyByDefault: true
originalSourceMap := settings.Config.Server.SourceMap
defer func() {
settings.Config.Server.SourceMap = originalSourceMap
access.ClearCache()
}()
settings.Config.Server.SourceMap = map[string]settings.Source{
"mnt/secure": {
Path: "mnt/secure",
Name: "secure",
Config: settings.SourceConfig{
DenyByDefault: true,
},
},
}
// 3. Set up access rules
if err := s.AllowUser("mnt/secure", "/", "admin"); err != nil {
t.Fatalf("AllowUser for admin failed: %v", err)
}
if err := s.AllowUser("mnt/secure", "/test", "graham"); err != nil {
t.Fatalf("AllowUser for graham failed: %v", err)
}
// 4. Assertions
// Admin checks
if !s.Permitted("mnt/secure", "/", "admin") {
t.Error("admin should be permitted for /")
}
if !s.Permitted("mnt/secure", "/test", "admin") {
t.Error("admin should be permitted for /test")
}
if !s.Permitted("mnt/secure", "/test/sub", "admin") {
t.Error("admin should be permitted for /test/sub (this is the bug)")
}
// Graham checks
if !s.Permitted("mnt/secure", "/test", "graham") {
t.Error("graham should be permitted for /test")
}
if !s.Permitted("mnt/secure", "/test/sub", "graham") {
t.Error("graham should be permitted for /test/sub")
}
if s.Permitted("mnt/secure", "/", "graham") {
t.Error("graham should NOT be permitted for /")
}
if s.Permitted("mnt/secure", "/anotherfolder", "graham") {
t.Error("graham should NOT be permitted for /anotherfolder")
}
// Bob checks (should have no access)
if s.Permitted("mnt/secure", "/", "bob") {
t.Error("bob should NOT be permitted for /")
}
if s.Permitted("mnt/secure", "/test", "bob") {
t.Error("bob should NOT be permitted for /test")
}
if s.Permitted("mnt/secure", "/test/sub", "bob") {
t.Error("bob should NOT be permitted for /test/sub")
}
}
+18 -6
View File
@@ -75,7 +75,12 @@ func Register(username string, sources []string) chan EventMessage {
func Unregister(username string, ch chan EventMessage) {
userClientsMu.Lock()
conns := userClients[username]
defer userClientsMu.Unlock()
conns, ok := userClients[username]
if !ok {
// User already cleaned up by Shutdown, nothing to do.
return
}
for i, c := range conns {
if c == ch {
userClients[username] = append(conns[:i], conns[i+1:]...)
@@ -85,18 +90,15 @@ func Unregister(username string, ch chan EventMessage) {
if len(userClients[username]) == 0 {
delete(userClients, username)
}
userClientsMu.Unlock()
sourceClientsMu.Lock()
defer sourceClientsMu.Unlock()
for source, clients := range sourceClients {
delete(clients, ch)
if len(clients) == 0 {
delete(sourceClients, source)
}
}
sourceClientsMu.Unlock()
close(ch)
}
func SendToUsers(eventType, message string, users []string) {
@@ -142,10 +144,20 @@ func handleSourceUpdates() {
func Shutdown() {
userClientsMu.Lock()
defer userClientsMu.Unlock()
sourceClientsMu.Lock()
defer sourceClientsMu.Unlock()
for _, clientChannels := range userClients {
for username, clientChannels := range userClients {
for _, ch := range clientChannels {
// Clean up source clients
for source, clients := range sourceClients {
delete(clients, ch)
if len(clients) == 0 {
delete(sourceClients, source)
}
}
close(ch)
}
delete(userClients, username)
}
}
+341
View File
@@ -0,0 +1,341 @@
package ffmpeg
import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
)
// ImageService handles image operations with ffmpeg
type ImageService struct {
ffmpegPath string
ffprobePath string
debug bool
}
// NewImageService creates a new image service instance
func NewImageService(ffmpegPath, ffprobePath string, debug bool) *ImageService {
return &ImageService{
ffmpegPath: ffmpegPath,
ffprobePath: ffprobePath,
debug: debug,
}
}
// GetImageDimensions extracts the dimensions of an image file using ffprobe
func (s *ImageService) GetImageDimensions(imagePath string) (width, height int, err error) {
fmt.Printf("🔍 IMAGE SERVICE: Getting dimensions for %s\n", filepath.Base(imagePath))
fmt.Printf("🔧 IMAGE SERVICE: Using ffprobe at: %s\n", s.ffprobePath)
// Get HEIC dimensions (fallback to individual stream dimensions for nowe re
probeCmd := exec.Command(
s.ffprobePath,
"-v", "error",
"-select_streams", "v:0",
"-show_entries", "stream=width,height",
"-of", "csv=p=0:s=x",
imagePath,
)
fmt.Printf("🚀 IMAGE SERVICE: Running command: %s %s\n", s.ffprobePath, strings.Join(probeCmd.Args[1:], " "))
var probeOut bytes.Buffer
var probeErr bytes.Buffer
probeCmd.Stdout = &probeOut
probeCmd.Stderr = &probeErr
if err = probeCmd.Run(); err != nil {
fmt.Printf("❌ IMAGE SERVICE: ffprobe failed with error: %v\n", err)
fmt.Printf("❌ IMAGE SERVICE: ffprobe stderr: %s\n", probeErr.String())
return 0, 0, fmt.Errorf("ffprobe command failed on image file '%s': %w", imagePath, err)
}
dimensions := strings.TrimSpace(probeOut.String())
fmt.Printf("📏 IMAGE SERVICE: Got raw dimensions string: '%s'\n", dimensions)
// Handle cases where ffprobe returns multiple lines or extra characters
// Take the first line and clean it up
lines := strings.Split(dimensions, "\n")
cleanDimensions := strings.TrimSpace(lines[0])
// Remove any trailing 'x' characters that might appear
cleanDimensions = strings.TrimSuffix(cleanDimensions, "x")
fmt.Printf("📏 IMAGE SERVICE: Cleaned dimensions string: '%s'\n", cleanDimensions)
parts := strings.Split(cleanDimensions, "x")
if len(parts) != 2 {
fmt.Printf("❌ IMAGE SERVICE: Invalid dimensions format after cleaning: %s\n", cleanDimensions)
return 0, 0, fmt.Errorf("invalid dimensions format: %s", cleanDimensions)
}
width, err = strconv.Atoi(parts[0])
if err != nil {
fmt.Printf("❌ IMAGE SERVICE: Invalid width: %s\n", parts[0])
return 0, 0, fmt.Errorf("invalid width: %w", err)
}
height, err = strconv.Atoi(parts[1])
if err != nil {
fmt.Printf("❌ IMAGE SERVICE: Invalid height: %s\n", parts[1])
return 0, 0, fmt.Errorf("invalid height: %w", err)
}
fmt.Printf("✅ IMAGE SERVICE: Parsed dimensions: %dx%d\n", width, height)
return width, height, nil
}
// ConvertHEICToJPEG converts a HEIC file to JPEG with specified dimensions and quality using proper tile extraction
func (s *ImageService) ConvertHEICToJPEG(heicPath string, targetWidth, targetHeight int, quality string) ([]byte, error) {
fmt.Printf("🎯 IMAGE SERVICE: Converting HEIC %s to JPEG (%dx%d, quality %s) using proper tile extraction\n", filepath.Base(heicPath), targetWidth, targetHeight, quality)
// Create temporary directory for tile processing
outputDir := filepath.Dir(heicPath)
tempDir := filepath.Join(outputDir, fmt.Sprintf("heic_tiles_%d", os.Getpid()))
err := os.MkdirAll(tempDir, 0755)
if err != nil {
return nil, fmt.Errorf("failed to create temp directory: %w", err)
}
defer func() {
fmt.Printf("🧹 IMAGE SERVICE: Cleaning up temporary directory: %s\n", tempDir)
os.RemoveAll(tempDir)
}()
// Step 1: Get grid info using trace output
fmt.Printf("📐 IMAGE SERVICE: Step 1 - Getting grid info from HEIC trace\n")
// Use ffprobe with trace level to get grid information
gridCmd := exec.Command(s.ffmpegPath, "-loglevel", "trace", "-i", heicPath, "-f", "null", "-")
var gridOut bytes.Buffer
var gridErr bytes.Buffer
gridCmd.Stdout = &gridOut
gridCmd.Stderr = &gridErr
_ = gridCmd.Run() // Don't check error, we're just extracting info
traceOutput := gridErr.String()
fmt.Printf("📊 IMAGE SERVICE: Got trace output: %d bytes\n", len(traceOutput))
// Parse grid dimensions from trace output
var gridCols, gridRows = 8, 6 // Default fallback
var val int
// Look for grid_row and grid_col in trace output
lines := strings.Split(traceOutput, "\n")
for _, line := range lines {
if strings.Contains(line, "grid_row") {
// Try to extract grid_row value
if parts := strings.Split(line, "grid_row"); len(parts) > 1 {
if matches := strings.Fields(parts[1]); len(matches) > 0 {
if val, err = strconv.Atoi(strings.Trim(matches[0], " :=")); err == nil {
gridRows = val + 1 // HEIC grid_row is 0-based
fmt.Printf("📐 IMAGE SERVICE: Parsed grid_row: %d (rows: %d)\n", val, gridRows)
}
}
}
}
if strings.Contains(line, "grid_col") {
// Try to extract grid_col value
if parts := strings.Split(line, "grid_col"); len(parts) > 1 {
if matches := strings.Fields(parts[1]); len(matches) > 0 {
if val, err = strconv.Atoi(strings.Trim(matches[0], " :=")); err == nil {
gridCols = val + 1 // HEIC grid_col is 0-based
fmt.Printf("📐 IMAGE SERVICE: Parsed grid_col: %d (cols: %d)\n", val, gridCols)
}
}
}
}
}
fmt.Printf("📊 IMAGE SERVICE: Using grid dimensions: %dx%d\n", gridCols, gridRows)
// Use FFmpeg's auto-orientation feature instead of forced rotation
fmt.Printf("🔄 IMAGE SERVICE: Using FFmpeg auto-orientation for proper image orientation\n")
orientationFilter := ",auto-orient=1"
// Step 2: Extract tile streams (skip stream 0 which is compatibility image)
fmt.Printf("🔧 IMAGE SERVICE: Step 2 - Extracting tile streams\n")
// Extract using the exact approach that works: ffmpeg -i input.heic -map 0 output_%d.png
// But we'll skip the first output (stream 0) since it's the compatibility image
tilesPattern := filepath.Join(tempDir, "output_%d.png")
extractCmd := exec.Command(
s.ffmpegPath,
"-i", heicPath,
"-map", "0", // Map all streams
"-y",
tilesPattern,
)
var extractOut bytes.Buffer
var extractErr bytes.Buffer
extractCmd.Stdout = &extractOut
extractCmd.Stderr = &extractErr
fmt.Printf("🚀 IMAGE SERVICE: Running tile extraction: %s %s\n", s.ffmpegPath, strings.Join(extractCmd.Args[1:], " "))
if err = extractCmd.Run(); err != nil {
fmt.Printf("❌ IMAGE SERVICE: Tile extraction failed: %v\n", err)
fmt.Printf("❌ IMAGE SERVICE: stderr: %s\n", extractErr.String())
return nil, fmt.Errorf("tile extraction failed: %w", err)
}
fmt.Printf("✅ IMAGE SERVICE: Tiles extracted successfully\n")
// Step 3: Filter tiles (skip output_0.png which is compatibility image)
fmt.Printf("🔧 IMAGE SERVICE: Step 3 - Filtering tiles\n")
files, err := filepath.Glob(filepath.Join(tempDir, "output_*.png"))
if err != nil || len(files) == 0 {
return nil, fmt.Errorf("no tiles extracted, found %d files", len(files))
}
fmt.Printf("📊 IMAGE SERVICE: Found %d extracted files\n", len(files))
// Create filtered tiles, skipping output_0.png (compatibility image)
// and rename them to sequential tile_001.png, tile_002.png, etc.
tileIndex := 1
expectedTiles := gridCols * gridRows
for i := 1; i <= len(files); i++ { // Start from 1 to skip output_0.png
sourceFile := filepath.Join(tempDir, fmt.Sprintf("output_%d.png", i))
var info os.FileInfo
if info, err = os.Stat(sourceFile); err == nil && info.Size() > 500 {
// This is a valid tile, rename it to sequential format
targetFile := filepath.Join(tempDir, fmt.Sprintf("tile_%03d.png", tileIndex))
if err = os.Rename(sourceFile, targetFile); err != nil {
fmt.Printf("⚠️ IMAGE SERVICE: Failed to rename %s: %v\n", filepath.Base(sourceFile), err)
continue
}
fmt.Printf("✅ IMAGE SERVICE: Valid tile %d: %s (%d bytes)\n", tileIndex, filepath.Base(targetFile), info.Size())
tileIndex++
// Stop if we have enough tiles
if tileIndex > expectedTiles {
break
}
} else {
fmt.Printf("⚠️ IMAGE SERVICE: Skipping invalid tile: output_%d.png\n", i)
}
}
actualTiles := tileIndex - 1
fmt.Printf("📊 IMAGE SERVICE: Using %d valid tiles (expected %d) with grid %dx%d\n", actualTiles, expectedTiles, gridCols, gridRows)
// Step 4: Merge tiles back into final image with proper quality and rotation
outputFile := filepath.Join(tempDir, "final_output.jpg")
// Build the video filter chain
inputPattern := filepath.Join(tempDir, "tile_%03d.png")
// Create filter chain: tile assembly + auto-orientation + proper scaling
var filterChain string
// For original size (targetWidth=0, targetHeight=0), don't scale
if targetWidth == 0 && targetHeight == 0 {
// This is original size - no scaling, maximum quality
// Use padding=0:margin=0 to eliminate black borders between tiles
filterChain = fmt.Sprintf("tile=%dx%d:nb_frames=%d:padding=0:margin=0%s",
gridCols, gridRows, actualTiles, orientationFilter)
fmt.Printf("🎯 IMAGE SERVICE: Using ORIGINAL size - no scaling, auto-orientation applied\n")
} else {
// This is a resize request - apply scaling with proper aspect handling
filterChain = fmt.Sprintf("tile=%dx%d:nb_frames=%d:padding=0:margin=0%s,scale=%d:%d:force_original_aspect_ratio=decrease:eval=frame",
gridCols, gridRows, actualTiles, orientationFilter, targetWidth, targetHeight)
fmt.Printf("🔽 IMAGE SERVICE: Scaling to %dx%d with auto-orientation applied\n", targetWidth, targetHeight)
}
fmt.Printf("🎨 IMAGE SERVICE: Using filter chain: %s\n", filterChain)
mergeCmd := exec.Command(
s.ffmpegPath,
"-i", inputPattern,
"-vf", filterChain,
"-frames:v", "1",
"-q:v", quality, // Use the quality setting passed in
"-pix_fmt", "yuvj420p", // Use full-range YUV for better JPEG compatibility
"-y",
outputFile,
)
var mergeOut bytes.Buffer
var mergeErr bytes.Buffer
mergeCmd.Stdout = &mergeOut
mergeCmd.Stderr = &mergeErr
fmt.Printf("🚀 IMAGE SERVICE: Running tile merge: %s %s\n", s.ffmpegPath, strings.Join(mergeCmd.Args[1:], " "))
if err = mergeCmd.Run(); err != nil {
fmt.Printf("❌ IMAGE SERVICE: Tile merge failed: %v\n", err)
fmt.Printf("❌ IMAGE SERVICE: stderr: %s\n", mergeErr.String())
return nil, fmt.Errorf("tile merge failed: %w", err)
}
fmt.Printf("✅ IMAGE SERVICE: Tiles merged successfully\n")
fmt.Printf("📖 IMAGE SERVICE: Reading final merged JPEG file: %s\n", outputFile)
jpegBytes, err := os.ReadFile(outputFile)
if err != nil {
fmt.Printf("❌ IMAGE SERVICE: Failed to read merged JPEG: %v\n", err)
return nil, fmt.Errorf("failed to read merged JPEG: %w", err)
}
fmt.Printf("✅ IMAGE SERVICE: Successfully read %d bytes from merged JPEG\n", len(jpegBytes))
return jpegBytes, nil
}
// ConvertImageToFormat converts any supported image format to another format
func (s *ImageService) ConvertImageToFormat(inputPath, outputFormat string, targetWidth, targetHeight int, quality string) ([]byte, error) {
// Create temporary output file
outputDir := filepath.Dir(inputPath)
ext := ".jpg"
if outputFormat == "png" {
ext = ".png"
}
outputFile := filepath.Join(outputDir, fmt.Sprintf("converted_%d_%d%s", targetWidth, targetHeight, ext))
defer os.Remove(outputFile) // Clean up
// Build FFmpeg command
args := []string{
"-i", inputPath,
}
// Add scaling and auto-orientation
if targetWidth > 0 && targetHeight > 0 {
scaleFilter := fmt.Sprintf("scale=%d:%d:force_original_aspect_ratio=decrease,auto-orient=1", targetWidth, targetHeight)
args = append(args, "-vf", scaleFilter)
} else {
args = append(args, "-vf", "auto-orient=1")
}
// Add quality settings
if quality != "" {
args = append(args, "-q:v", quality)
}
// Add output file and overwrite flag
args = append(args, "-y", outputFile)
cmd := exec.Command(s.ffmpegPath, args...)
if s.debug {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("ffmpeg image conversion failed on file '%s': %w", inputPath, err)
}
// Read the converted file
convertedBytes, err := os.ReadFile(outputFile)
if err != nil {
return nil, fmt.Errorf("failed to read converted image: %w", err)
}
return convertedBytes, nil
}
+33 -20
View File
@@ -3,6 +3,7 @@ module github.com/gtsteffaniak/filebrowser/backend
go 1.25.0
require (
github.com/adrium/goheif v0.0.0-20230113233934-ca402e77a786
github.com/asdine/storm/v3 v3.2.1
github.com/coreos/go-oidc/v3 v3.15.0
github.com/dsoprea/go-exif/v3 v3.0.1
@@ -15,16 +16,16 @@ require (
github.com/gtsteffaniak/go-logger v0.1.2
github.com/kovidgoyal/imaging v1.6.4
github.com/pquerna/otp v1.5.0
github.com/spf13/afero v1.14.0
github.com/stretchr/testify v1.10.0
github.com/spf13/afero v1.15.0
github.com/stretchr/testify v1.11.1
github.com/swaggo/http-swagger v1.3.4
github.com/swaggo/swag v1.16.6
golang.org/x/crypto v0.41.0
golang.org/x/image v0.30.0
golang.org/x/mod v0.27.0
golang.org/x/oauth2 v0.30.0
golang.org/x/sys v0.35.0
golang.org/x/time v0.12.0
golang.org/x/crypto v0.42.0
golang.org/x/image v0.31.0
golang.org/x/mod v0.28.0
golang.org/x/oauth2 v0.31.0
golang.org/x/sys v0.36.0
golang.org/x/time v0.13.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -46,7 +47,7 @@ require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Masterminds/semver/v3 v3.3.1 // indirect
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
github.com/air-verse/air v1.62.0 // indirect
github.com/air-verse/air v1.63.0 // indirect
github.com/alecthomas/chroma/v2 v2.20.0 // indirect
github.com/alecthomas/go-check-sumtype v0.3.1 // indirect
github.com/alexkohler/nakedret/v2 v2.0.6 // indirect
@@ -101,10 +102,21 @@ require (
github.com/go-critic/go-critic v0.13.0 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
github.com/go-openapi/jsonpointer v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/jsonpointer v0.22.0 // indirect
github.com/go-openapi/jsonreference v0.21.1 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag v0.24.1 // indirect
github.com/go-openapi/swag/cmdutils v0.24.0 // indirect
github.com/go-openapi/swag/conv v0.24.0 // indirect
github.com/go-openapi/swag/fileutils v0.24.0 // indirect
github.com/go-openapi/swag/jsonname v0.24.0 // indirect
github.com/go-openapi/swag/jsonutils v0.24.0 // indirect
github.com/go-openapi/swag/loading v0.24.0 // indirect
github.com/go-openapi/swag/mangling v0.24.0 // indirect
github.com/go-openapi/swag/netutils v0.24.0 // indirect
github.com/go-openapi/swag/stringutils v0.24.0 // indirect
github.com/go-openapi/swag/typeutils v0.24.0 // indirect
github.com/go-openapi/swag/yamlutils v0.24.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-toolsmith/astcast v1.1.0 // indirect
@@ -118,8 +130,8 @@ require (
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/gohugoio/hugo v0.147.6 // indirect
github.com/golang/geo v0.0.0-20250825151631-54d70cc7cb31 // indirect
github.com/gohugoio/hugo v0.149.1 // indirect
github.com/golang/geo v0.0.0-20250909101022-21d0664d86be // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
github.com/golangci/go-printf-func-name v0.1.0 // indirect
@@ -200,6 +212,7 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect
github.com/ryancurrah/gomodguard v1.4.1 // indirect
github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect
github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect
@@ -211,7 +224,7 @@ require (
github.com/sivchari/containedctx v1.0.3 // indirect
github.com/sonatard/noctx v0.4.0 // indirect
github.com/sourcegraph/go-diff v0.7.0 // indirect
github.com/spf13/cast v1.8.0 // indirect
github.com/spf13/cast v1.9.2 // indirect
github.com/spf13/cobra v1.9.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.7 // indirect
@@ -222,7 +235,7 @@ require (
github.com/subosito/gotenv v1.4.1 // indirect
github.com/swaggo/files v1.0.1 // indirect
github.com/tdakkota/asciicheck v0.4.1 // indirect
github.com/tdewolff/parse/v2 v2.8.1 // indirect
github.com/tdewolff/parse/v2 v2.8.3 // indirect
github.com/tetafro/godot v1.5.1 // indirect
github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect
github.com/timonwong/loggercheck v0.11.0 // indirect
@@ -248,11 +261,11 @@ require (
go.uber.org/multierr v1.10.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.36.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
honnef.co/go/tools v0.6.1 // indirect
+96 -59
View File
@@ -73,8 +73,8 @@ github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsu
github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
github.com/air-verse/air v1.62.0 h1:6CoXL4MAX9dc4xAzLfjMcDfbBoGmW5VjuuTV/1+bI+M=
github.com/air-verse/air v1.62.0/go.mod h1:EO+jWuetL10tS9raffwg8WEV0t0KUeucRRaf9ii86dA=
github.com/air-verse/air v1.63.0 h1:fwcdHpwaUe4/+q349PxptzAIn8gVE6Yke8TgW0LsDxQ=
github.com/air-verse/air v1.63.0/go.mod h1:RyCQVx2+3Zz2BzoqkukYiGmWkWXNKMf0x5ubIFcUB8Q=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
@@ -108,6 +108,8 @@ github.com/ashanbrown/makezero/v2 v2.0.1 h1:r8GtKetWOgoJ4sLyUx97UTwyt2dO7WkGFHiz
github.com/ashanbrown/makezero/v2 v2.0.1/go.mod h1:kKU4IMxmYW1M4fiEHMb2vc5SFoPzXvgbMR9gIp5pjSw=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -116,8 +118,8 @@ github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps=
github.com/bep/clocks v0.5.0/go.mod h1:SUq3q+OOq41y2lRQqH5fsOoxN8GbxSiT6jvoVVLCVhU=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/bep/gitmap v1.6.0 h1:sDuQMm9HoTL0LtlrfxjbjgAg2wHQd4nkMup2FInYzhA=
github.com/bep/gitmap v1.6.0/go.mod h1:n+3W1f/rot2hynsqEGxGMErPRgT41n9CkGuzPvz9cIw=
github.com/bep/gitmap v1.9.0 h1:2pyb1ex+cdwF6c4tsrhEgEKfyNfxE34d5K+s2sa9byc=
github.com/bep/gitmap v1.9.0/go.mod h1:Juq6e1qqCRvc1W7nzgadPGI9IGV13ZncEebg5atj4Vo=
github.com/bep/goat v0.5.0 h1:S8jLXHCVy/EHIoCY+btKkmcxcXFd34a0Q63/0D4TKeA=
github.com/bep/goat v0.5.0/go.mod h1:Md9x7gRxiWKs85yHlVTvHQw9rg86Bm+Y4SuYE8CTH7c=
github.com/bep/godartsass/v2 v2.5.0 h1:tKRvwVdyjCIr48qgtLa4gHEdtRkPF8H1OeEhJAEv7xg=
@@ -128,6 +130,8 @@ github.com/bep/goportabletext v0.1.0 h1:8dqym2So1cEqVZiBa4ZnMM1R9l/DnC1h4ONg4J5k
github.com/bep/goportabletext v0.1.0/go.mod h1:6lzSTsSue75bbcyvVc0zqd1CdApuT+xkZQ6Re5DzZFg=
github.com/bep/gowebp v0.4.0 h1:QihuVnvIKbRoeBNQkN0JPMM8ClLmD6V2jMftTFwSK3Q=
github.com/bep/gowebp v0.4.0/go.mod h1:95gtYkAA8iIn1t3HkAPurRCVGV/6NhgaHJ1urz0iIwc=
github.com/bep/helpers v0.6.0 h1:qtqMCK8XPFNM9hp5Ztu9piPjxNNkk8PIyUVjg6v8Bsw=
github.com/bep/helpers v0.6.0/go.mod h1:IOZlgx5PM/R/2wgyCatfsgg5qQ6rNZJNDpWGXqDR044=
github.com/bep/imagemeta v0.12.0 h1:ARf+igs5B7pf079LrqRnwzQ/wEB8Q9v4NSDRZO1/F5k=
github.com/bep/imagemeta v0.12.0/go.mod h1:23AF6O+4fUi9avjiydpKLStUNtJr5hJB4rarG18JpN8=
github.com/bep/lazycache v0.8.0 h1:lE5frnRjxaOFbkPZ1YL6nijzOPPz6zeXasJq8WpG4L8=
@@ -236,8 +240,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q=
github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A=
github.com/evanw/esbuild v0.25.3 h1:4JKyUsm/nHDhpxis4IyWXAi8GiyTwG1WdEp6OhGVE8U=
github.com/evanw/esbuild v0.25.3/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
github.com/evanw/esbuild v0.25.9 h1:aU7GVC4lxJGC1AyaPwySWjSIaNLAdVEEuq3chD0Khxs=
github.com/evanw/esbuild v0.25.9/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
@@ -255,8 +259,8 @@ github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIp
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gen2brain/go-fitz v1.24.15 h1:sJNB1MOWkqnzzENPHggFpgxTwW0+S5WF/rM5wUBpJWo=
github.com/gen2brain/go-fitz v1.24.15/go.mod h1:SftkiVbTHqF141DuiLwBBM65zP7ig6AVDQpf2WlHamo=
github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk=
github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghostiam/protogetter v0.3.15 h1:1KF5sXel0HE48zh1/vn0Loiw25A9ApyseLzQuif1mLY=
@@ -282,14 +286,36 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v0.21.2 h1:AqQaNADVwq/VnkCmQg6ogE+M3FOsKTytwges0JdwVuA=
github.com/go-openapi/jsonpointer v0.21.2/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM=
github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU=
github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA=
github.com/go-openapi/jsonreference v0.21.1/go.mod h1:PWs8rO4xxTUqKGu+lEvvCxD5k2X7QYkKAepJyCmSTT8=
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8=
github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A=
github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I=
github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8=
github.com/go-openapi/swag/conv v0.24.0 h1:ejB9+7yogkWly6pnruRX45D1/6J+ZxRu92YFivx54ik=
github.com/go-openapi/swag/conv v0.24.0/go.mod h1:jbn140mZd7EW2g8a8Y5bwm8/Wy1slLySQQ0ND6DPc2c=
github.com/go-openapi/swag/fileutils v0.24.0 h1:U9pCpqp4RUytnD689Ek/N1d2N/a//XCeqoH508H5oak=
github.com/go-openapi/swag/fileutils v0.24.0/go.mod h1:3SCrCSBHyP1/N+3oErQ1gP+OX1GV2QYFSnrTbzwli90=
github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k=
github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q=
github.com/go-openapi/swag/jsonutils v0.24.0 h1:F1vE1q4pg1xtO3HTyJYRmEuJ4jmIp2iZ30bzW5XgZts=
github.com/go-openapi/swag/jsonutils v0.24.0/go.mod h1:vBowZtF5Z4DDApIoxcIVfR8v0l9oq5PpYRUuteVu6f0=
github.com/go-openapi/swag/loading v0.24.0 h1:ln/fWTwJp2Zkj5DdaX4JPiddFC5CHQpvaBKycOlceYc=
github.com/go-openapi/swag/loading v0.24.0/go.mod h1:gShCN4woKZYIxPxbfbyHgjXAhO61m88tmjy0lp/LkJk=
github.com/go-openapi/swag/mangling v0.24.0 h1:PGOQpViCOUroIeak/Uj/sjGAq9LADS3mOyjznmHy2pk=
github.com/go-openapi/swag/mangling v0.24.0/go.mod h1:Jm5Go9LHkycsz0wfoaBDkdc4CkpuSnIEf62brzyCbhc=
github.com/go-openapi/swag/netutils v0.24.0 h1:Bz02HRjYv8046Ycg/w80q3g9QCWeIqTvlyOjQPDjD8w=
github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM=
github.com/go-openapi/swag/stringutils v0.24.0 h1:i4Z/Jawf9EvXOLUbT97O0HbPUja18VdBxeadyAqS1FM=
github.com/go-openapi/swag/stringutils v0.24.0/go.mod h1:5nUXB4xA0kw2df5PRipZDslPJgJut+NjL7D25zPZ/4w=
github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zibnEas2Jm/wIw=
github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI=
github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c=
github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -341,10 +367,10 @@ github.com/gohugoio/hashstructure v0.5.0 h1:G2fjSBU36RdwEJBWJ+919ERvOVqAg9tfcYp4
github.com/gohugoio/hashstructure v0.5.0/go.mod h1:Ser0TniXuu/eauYmrwM4o64EBvySxNzITEOLlm4igec=
github.com/gohugoio/httpcache v0.7.0 h1:ukPnn04Rgvx48JIinZvZetBfHaWE7I01JR2Q2RrQ3Vs=
github.com/gohugoio/httpcache v0.7.0/go.mod h1:fMlPrdY/vVJhAriLZnrF5QpN3BNAcoBClgAyQd+lGFI=
github.com/gohugoio/hugo v0.147.6 h1:rL4rnus/5qzj4+FoA+JMzsVvFJ2YZdVIH6pbuCB2P84=
github.com/gohugoio/hugo v0.147.6/go.mod h1:Sb2COQPDPYG+tRSpePtzKytiuVDqkBivEhgIew1QbNo=
github.com/gohugoio/hugo-goldmark-extensions/extras v0.3.0 h1:gj49kTR5Z4Hnm0ZaQrgPVazL3DUkppw+x6XhHCmh+Wk=
github.com/gohugoio/hugo-goldmark-extensions/extras v0.3.0/go.mod h1:IMMj7xiUbLt1YNJ6m7AM4cnsX4cFnnfkleO/lBHGzUg=
github.com/gohugoio/hugo v0.149.1 h1:uWOc8Ve4h4e48FyYhBquRoHCJviyxA5yGrFJLT48yio=
github.com/gohugoio/hugo v0.149.1/go.mod h1:HS6BP6e8FGxungP4CHC3zeLDvhBLnTJIjHJZWTZjs7o=
github.com/gohugoio/hugo-goldmark-extensions/extras v0.5.0 h1:dco+7YiOryRoPOMXwwaf+kktZSCtlFtreNdiJbETvYE=
github.com/gohugoio/hugo-goldmark-extensions/extras v0.5.0/go.mod h1:CRrxQTKeM3imw+UoS4EHKyrqB7Zp6sAJiqHit+aMGTE=
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.1 h1:nUzXfRTszLliZuN0JTKeunXTRaiFX6ksaWP0puLLYAY=
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.1/go.mod h1:Wy8ThAA8p2/w1DY05vEzq6EIeI2mzDjvHsu7ULBVwog=
github.com/gohugoio/locales v0.14.0 h1:Q0gpsZwfv7ATHMbcTNepFd59H7GoykzWJIxi113XGDc=
@@ -356,8 +382,8 @@ github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20250825151631-54d70cc7cb31 h1:226lwSa0uZO7sd7QU88n43nDIqGoc+bOh0vSO3Q/byU=
github.com/golang/geo v0.0.0-20250825151631-54d70cc7cb31/go.mod h1:AN0OjM34c3PbjAsX+QNma1nYtJtRxl+s9MZNV7S+efw=
github.com/golang/geo v0.0.0-20250909101022-21d0664d86be h1:LYVqvM1o7RQN4bdv0iQbd7+x2aQ5m0mi5aQp5r/oR9U=
github.com/golang/geo v0.0.0-20250909101022-21d0664d86be/go.mod h1:AN0OjM34c3PbjAsX+QNma1nYtJtRxl+s9MZNV7S+efw=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -395,7 +421,6 @@ github.com/golangci/go-printf-func-name v0.1.0 h1:dVokQP+NMTO7jwO4bwsRwLWeudOVUP
github.com/golangci/go-printf-func-name v0.1.0/go.mod h1:wqhWFH5mUdJQhweRnldEywnR5021wTdZSNgwYceV14s=
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE=
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY=
github.com/golangci/golangci-lint v1.64.8 h1:y5TdeVidMtBGG32zgSC7ZXTFNHrsJkDnpO4ItB3Am+I=
github.com/golangci/golangci-lint/v2 v2.4.0 h1:qz6O6vr7kVzXJqyvHjHSz5fA3D+PM8v96QU5gxZCNWM=
github.com/golangci/golangci-lint/v2 v2.4.0/go.mod h1:Oq7vuAf6L1iNL34uHDcsIF6Mnc0amOPdsT3/GlpHD+I=
github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95 h1:AkK+w9FZBXlU/xUmBtSJN1+tAI4FIvy5WtnUnY8e4p8=
@@ -442,6 +467,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s=
github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk=
github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc=
github.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado=
@@ -579,6 +606,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0j
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgechev/revive v1.11.0 h1:b/gLLpBE427o+Xmd8G58gSA+KtBwxWinH/A565Awh0w=
github.com/mgechev/revive v1.11.0/go.mod h1:tI0oLF/2uj+InHCBLrrqfTKfjtFTBCFFfG05auyzgdw=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE=
@@ -600,8 +629,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U=
github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE=
github.com/niklasfasching/go-org v1.7.0 h1:vyMdcMWWTe/XmANk19F4k8XGBYg0GQ/gJGMimOjGMek=
github.com/niklasfasching/go-org v1.7.0/go.mod h1:WuVm4d45oePiE0eX25GqTDQIt/qPW1T9DGkRscqLW5o=
github.com/niklasfasching/go-org v1.9.1 h1:/3s4uTPOF06pImGa2Yvlp24yKXZoTYM+nsIlMzfpg/0=
github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48=
github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhKRf3Swg=
github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs=
github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk=
@@ -612,8 +641,12 @@ github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//J
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8=
github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
@@ -688,6 +721,8 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/ryancurrah/gomodguard v1.4.1 h1:eWC8eUMNZ/wM/PWuZBv7JxxqT5fiIKSIyTvjb7Elr+g=
github.com/ryancurrah/gomodguard v1.4.1/go.mod h1:qnMJwV1hX9m+YJseXEBhd2s90+1Xn6x9dLz11ualI1I=
github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU=
@@ -718,10 +753,10 @@ github.com/sonatard/noctx v0.4.0 h1:7MC/5Gg4SQ4lhLYR6mvOP6mQVSxCrdyiExo7atBs27o=
github.com/sonatard/noctx v0.4.0/go.mod h1:64XdbzFb18XL4LporKXp8poqZtPKbCrqQ402CV+kJas=
github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0=
github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=
github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
@@ -744,8 +779,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
@@ -756,10 +791,10 @@ github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/tdakkota/asciicheck v0.4.1 h1:bm0tbcmi0jezRA2b5kg4ozmMuGAFotKI3RZfrhfovg8=
github.com/tdakkota/asciicheck v0.4.1/go.mod h1:0k7M3rCfRXb0Z6bwgvkEIMleKH3kXNz9UqJ9Xuqopr8=
github.com/tdewolff/minify/v2 v2.23.5 h1:/P548KcpTkIOUvNg22zN83/GiaYSOIrbqtoue4I7kYM=
github.com/tdewolff/minify/v2 v2.23.5/go.mod h1:2RI9tiIrzJU1Z5EasXEPaI1MqobRyxKHOOgrRkq5oEw=
github.com/tdewolff/parse/v2 v2.8.1 h1:J5GSHru6o3jF1uLlEKVXkDxxcVx6yzOlIVIotK4w2po=
github.com/tdewolff/parse/v2 v2.8.1/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
github.com/tdewolff/minify/v2 v2.24.2 h1:vnY3nTulEAbCAAlxTxPPDkzG24rsq31SOzp63yT+7mo=
github.com/tdewolff/minify/v2 v2.24.2/go.mod h1:1JrCtoZXaDbqioQZfk3Jdmr0GPJKiU7c1Apmb+7tCeE=
github.com/tdewolff/parse/v2 v2.8.3 h1:5VbvtJ83cfb289A1HzRA9sf02iT8YyUwN84ezjkdY1I=
github.com/tdewolff/parse/v2 v2.8.3/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE=
github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA=
@@ -790,6 +825,8 @@ github.com/uudashr/iface v1.4.1 h1:J16Xl1wyNX9ofhpHmQ9h9gk5rnv2A6lX/2+APLTo0zU=
github.com/uudashr/iface v1.4.1/go.mod h1:pbeBPlbuU2qkNDn0mmfrxP2X+wjPMIQAy+r1MBXSXtg=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0=
github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM=
github.com/xen0n/gosmopolitan v1.3.0/go.mod h1:rckfr5T6o4lBtM1ga7mLGKZmLxswUoH1zxHgNXOsEt4=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
@@ -807,8 +844,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.11 h1:ZCxLyDMtz0nT2HFfsYG8WZ47Trip2+JyLysKcMYE5bo=
github.com/yuin/goldmark v1.7.11/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo=
@@ -848,8 +885,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -860,16 +897,16 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b h1:QoALfVG9rhQ/M7vYDScfPdWjGL9dlsVVM5VGh7aKoAA=
golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b h1:KdrhdYPDUvJTvrDK9gdjfFd6JTk8vA1WJoldYSi0kHo=
golang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b/go.mod h1:LKZHyeOpPuZcMgxeHjJp4p5yvxrCX1xDvH10zYHhjjQ=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
golang.org/x/image v0.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA=
golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -895,8 +932,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -939,16 +976,16 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -964,8 +1001,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1019,8 +1056,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -1037,13 +1074,13 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -1185,8 +1222,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+38 -35
View File
@@ -7,6 +7,7 @@ import (
"io"
"net/http"
"net/url"
"path/filepath"
"strconv"
"strings"
@@ -49,7 +50,21 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
return http.StatusBadRequest, errors.New("missing required parameters: path + source/hash are required")
}
themeMode := utils.Ternary(d.user.DarkMode, "dark", "light")
var sourceInfo settings.Source
var ok bool
if d.fileInfo.Hash != "" {
sourceInfo, ok = settings.Config.Server.SourceMap[source]
if !ok {
logger.Error("OnlyOffice: source not found")
return http.StatusInternalServerError, fmt.Errorf("source not found")
}
} else {
sourceInfo, ok = settings.Config.Server.NameToSource[source]
if !ok {
logger.Error("OnlyOffice: source not found")
return http.StatusInternalServerError, fmt.Errorf("source not found")
}
}
if d.fileInfo.Hash == "" {
// Build file info based on whether this is a share or regular request
// Regular user request - need to resolve scope
@@ -58,27 +73,20 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
logger.Errorf("OnlyOffice: source %s not available for user %s: %v", source, d.user.Username, scopeErr)
return http.StatusForbidden, fmt.Errorf("source %s is not available", source)
}
resolvedPath := utils.JoinPathAsUnix(userScope, path)
logger.Debugf("OnlyOffice user request: resolved path=%s", resolvedPath)
indexPath := utils.JoinPathAsUnix(userScope, path)
logger.Debugf("OnlyOffice user request: resolved path=%s", indexPath)
fileInfo, err := files.FileInfoFaster(iteminfo.FileOptions{
Path: resolvedPath,
Path: indexPath,
Modify: d.user.Permissions.Modify,
Source: source,
Expand: false,
})
if err != nil {
logger.Errorf("OnlyOffice: failed to get file info for source=%s, path=%s: %v", source, resolvedPath, err)
logger.Errorf("OnlyOffice: failed to get file info for source=%s, path=%s: %v", source, indexPath, err)
return errToStatus(err), err
}
d.fileInfo = *fileInfo
} else {
// is a share, use the file info from the share middleware
sourceInfo, ok := settings.Config.Server.SourceMap[d.share.Source]
if !ok {
logger.Error("OnlyOffice: source from share not found")
return http.StatusInternalServerError, fmt.Errorf("source not found for share")
}
source = sourceInfo.Name
// path is index path, so we build from share path
path = utils.JoinPathAsUnix(d.share.Path, path)
@@ -92,7 +100,7 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
}
// Determine file type and editing permissions
fileType := getFileExtension(d.fileInfo.Name)
fileType := strings.TrimPrefix(filepath.Ext(d.fileInfo.Name), ".")
canEdit := iteminfo.CanEditOnlyOffice(d.user.Permissions.Modify, fileType)
canEditMode := utils.Ternary(canEdit, "edit", "view")
if d.fileInfo.Hash != "" {
@@ -111,7 +119,7 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
}
// Generate document ID for OnlyOffice
documentId, err := getOnlyOfficeId(source, path)
documentId, err := getOnlyOfficeId(d.fileInfo.RealPath)
if err != nil {
logger.Errorf("OnlyOffice: failed to generate document ID for source=%s, path=%s: %v", source, path, err)
return http.StatusNotFound, fmt.Errorf("failed to generate document ID: %v", err)
@@ -166,15 +174,6 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
return renderJSON(w, r, clientConfig)
}
// getFileExtension extracts file extension from filename
func getFileExtension(filename string) string {
parts := strings.Split(filename, ".")
if len(parts) < 2 {
return ""
}
return parts[len(parts)-1]
}
// buildOnlyOfficeDownloadURL constructs the download URL that OnlyOffice server will use to fetch the file
func buildOnlyOfficeDownloadURL(source, path, hash, token string) string {
// Determine base URL (internal URL takes priority for OnlyOffice server communication)
@@ -254,6 +253,21 @@ func onlyofficeCallbackHandler(w http.ResponseWriter, r *http.Request, d *reques
logger.Errorf("OnlyOffice callback missing required parameters: source=%s, path=%s", source, path)
return http.StatusBadRequest, errors.New("missing required parameters: path + source/hash are required")
}
var sourceInfo settings.Source
var ok bool
if d.fileInfo.Hash != "" {
sourceInfo, ok = settings.Config.Server.SourceMap[source]
if !ok {
logger.Error("OnlyOffice: source not found")
return http.StatusInternalServerError, fmt.Errorf("source not found")
}
} else {
sourceInfo, ok = settings.Config.Server.NameToSource[source]
if !ok {
logger.Error("OnlyOffice: source not found")
return http.StatusInternalServerError, fmt.Errorf("source not found")
}
}
if d.fileInfo.Hash == "" {
// Regular user request - need to resolve scope
@@ -264,12 +278,6 @@ func onlyofficeCallbackHandler(w http.ResponseWriter, r *http.Request, d *reques
}
path = utils.JoinPathAsUnix(userScope, path)
} else {
// is a share, use the file info from the share middleware
sourceInfo, ok := settings.Config.Server.SourceMap[d.share.Source]
if !ok {
logger.Error("OnlyOffice: source from share not found")
return http.StatusInternalServerError, fmt.Errorf("source not found for share")
}
source = sourceInfo.Name
// path is index path, so we build from share path
path = utils.JoinPathAsUnix(d.share.Path, path)
@@ -339,12 +347,7 @@ func onlyofficeCallbackHandler(w http.ResponseWriter, r *http.Request, d *reques
return renderJSON(w, r, resp)
}
func getOnlyOfficeId(source, path string) (string, error) {
idx := indexing.GetIndex(source)
if idx == nil {
return "", fmt.Errorf("source not found")
}
realpath, _, _ := idx.GetRealPath(path)
func getOnlyOfficeId(realpath string) (string, error) {
// error is intentionally ignored in order treat errors
// the same as a cache-miss
cachedDocumentKey, ok := utils.OnlyOfficeCache.Get(realpath).(string)
+10 -2
View File
@@ -270,11 +270,16 @@ func (i *ItemInfo) DetectType(realPath string, saveContent bool) {
ext := filepath.Ext(name)
// Attempt MIME detection by file extension
if ext == ".md" {
switch ext {
case ".md":
i.Type = "text/markdown"
return
case ".heic", ".heif":
i.Type = "image/heic"
return
}
i.Type = strings.Split(mime.TypeByExtension(ext), ";")[0]
if i.Type == "" {
i.Type = ExtendedMimeTypeCheck(ext)
}
@@ -285,10 +290,13 @@ func (i *ItemInfo) DetectType(realPath string, saveContent bool) {
i.Type = DetectTypeByHeader(realPath)
return
}
if i.Type == "blob" {
if i.Type == "blob" || i.Type == "" {
i.Type = DetectTypeByHeader(realPath)
}
}
if i.Type == "" || i.Type == "application/octet-stream" {
i.Type = "blob"
}
}
// DetectTypeByHeader detects the MIME type of a file based on its header.
+29
View File
@@ -0,0 +1,29 @@
package preview
import (
"path/filepath"
"strings"
"github.com/gtsteffaniak/filebrowser/backend/indexing/iteminfo"
"github.com/gtsteffaniak/go-logger/logger"
)
// isHEICFile checks if the file is a HEIC file based on MIME type or extension
func isHEICFile(file iteminfo.ExtendedFileInfo) bool {
// Check by MIME type first
if file.Type == "image/heic" {
return true
}
// Check by file extension (in case MIME type detection fails)
ext := strings.ToLower(filepath.Ext(file.Name))
return ext == ".heic" || ext == ".heif"
}
// processHEICFile handles HEIC file processing by delegating to FFmpeg conversion
func (s *Service) processHEICFile(file iteminfo.ExtendedFileInfo, previewSize string) ([]byte, error) {
logger.Infof("🔄 HEIC: Processing HEIC file %s (size: %s)", filepath.Base(file.Name), previewSize)
// Delegate to FFmpeg conversion logic
return s.convertHEICToJPEGWithFFmpeg(file.RealPath, previewSize)
}
+66
View File
@@ -0,0 +1,66 @@
package preview
import (
"context"
"fmt"
"path/filepath"
"github.com/gtsteffaniak/filebrowser/backend/ffmpeg"
"github.com/gtsteffaniak/go-logger/logger"
)
// convertHEICToJPEGWithFFmpeg converts a HEIC file to JPEG format using FFmpeg
// This function handles all FFmpeg-related logic and parameters
func (s *Service) convertHEICToJPEGWithFFmpeg(filePath string, previewSize string) ([]byte, error) {
logger.Infof("🎯 HEIC FFMPEG: Starting conversion for %s (size: %s)", filepath.Base(filePath), previewSize)
if s.ffmpegPath == "" {
logger.Errorf("❌ HEIC FFMPEG: FFmpeg path is empty")
return nil, fmt.Errorf("FFmpeg is not available for HEIC conversion")
}
logger.Infof("🔧 HEIC FFMPEG: Using FFmpeg at: %s", s.ffmpegPath)
if err := s.acquire(context.Background()); err != nil {
logger.Errorf("❌ HEIC FFMPEG: Failed to acquire semaphore: %v", err)
return nil, err
}
defer s.release()
// Create FFmpeg image service
imageService := ffmpeg.NewImageService(s.ffmpegPath, s.ffprobePath, s.debug)
// Determine target dimensions and quality based on preview size
var width, height int
var quality string
switch previewSize {
case "large":
width, height = 640, 640
quality = "2" // High quality for FFmpeg -q:v
logger.Infof("📐 HEIC FFMPEG: Using LARGE size - target: %dx%d, quality: %s", width, height, quality)
case "small":
width, height = 256, 256
quality = "5" // Medium quality
logger.Infof("📐 HEIC FFMPEG: Using SMALL size - target: %dx%d, quality: %s", width, height, quality)
case "original":
// For original size - no scaling, maximum quality
logger.Infof("📏 HEIC FFMPEG: Using ORIGINAL size - no scaling, maximum quality")
width, height = 0, 0 // Signal to not apply scaling
quality = "1" // Maximum quality for original
logger.Infof("📐 HEIC FFMPEG: Using ORIGINAL mode - no scaling, quality: %s", quality)
default:
logger.Errorf("❌ HEIC FFMPEG: Unsupported preview size: %s", previewSize)
return nil, ErrUnsupportedFormat
}
logger.Infof("🚀 HEIC FFMPEG: Starting FFmpeg conversion with dimensions %dx%d, quality %s", width, height, quality)
result, err := imageService.ConvertHEICToJPEG(filePath, width, height, quality)
if err != nil {
logger.Errorf("❌ HEIC FFMPEG: Conversion failed: %v", err)
return nil, err
}
logger.Infof("✅ HEIC FFMPEG: Conversion successful, output size: %d bytes", len(result))
return result, nil
}
+23 -4
View File
@@ -1,4 +1,3 @@
//go:generate go-enum --sql --marshal --file $GOFILE
package preview
import (
@@ -8,9 +7,9 @@ import (
"image"
"io"
"github.com/kovidgoyal/imaging"
exif "github.com/dsoprea/go-exif/v3"
exifcommon "github.com/dsoprea/go-exif/v3/common"
"github.com/kovidgoyal/imaging"
)
// Format is an image file format.
@@ -21,6 +20,7 @@ png
gif
tiff
bmp
heic
)
*/
type Format int
@@ -37,6 +37,8 @@ func (x Format) toImaging() imaging.Format {
return imaging.TIFF
case FormatBmp:
return imaging.BMP
case FormatHeic:
return imaging.JPEG
default:
return imaging.JPEG
}
@@ -73,6 +75,12 @@ fill
type ResizeMode int
func (s *Service) FormatFromExtension(ext string) (Format, error) {
// heic is not supported by imaging, so we return FormatHeic
switch ext {
case ".heic", ".heif":
return FormatHeic, nil
}
format, err := imaging.FormatFromExtension(ext)
if err != nil {
return -1, ErrUnsupportedFormat
@@ -139,6 +147,10 @@ func (s *Service) Resize(in io.Reader, width, height int, out io.Writer, options
option(&config)
}
if format == FormatHeic {
config.format = FormatJpeg
}
if config.quality == QualityLow && format == FormatJpeg {
thm, newWrappedReader, errThm := getEmbeddedThumbnail(wrappedReader)
wrappedReader = newWrappedReader
@@ -150,16 +162,19 @@ func (s *Service) Resize(in io.Reader, width, height int, out io.Writer, options
}
}
img, err := imaging.Decode(wrappedReader, imaging.AutoOrientation(true))
// For HEIC files, try without AutoOrientation first since it might not work properly
img, err := imaging.Decode(wrappedReader)
if err != nil {
return err
}
// Note: For HEIC files processed via FFmpeg, orientation is handled automatically
switch config.resizeMode {
case ResizeModeFill:
img = imaging.Fill(img, width, height, imaging.Center, config.quality.resampleFilter())
case ResizeModeFit:
fallthrough
img = imaging.Fit(img, width, height, config.quality.resampleFilter())
default:
img = imaging.Fit(img, width, height, config.quality.resampleFilter())
}
@@ -176,6 +191,10 @@ func (s *Service) detectFormat(in io.Reader) (Format, io.Reader, error) {
return 0, nil, fmt.Errorf("%s: %w", err.Error(), ErrUnsupportedFormat)
}
if imgFormat == "heif" {
imgFormat = "heic"
}
format, err := ParseFormat(imgFormat)
if err != nil {
return 0, nil, ErrUnsupportedFormat
+15 -11
View File
@@ -19,16 +19,19 @@ const (
FormatTiff
// FormatBmp is a Format of type Bmp
FormatBmp
// FormatHeic is a Format of type Heic
FormatHeic
)
const _FormatName = "jpegpnggiftiffbmp"
const _FormatName = "jpegpnggiftiffbmpheic"
var _FormatMap = map[Format]string{
0: _FormatName[0:4],
1: _FormatName[4:7],
2: _FormatName[7:10],
3: _FormatName[10:14],
4: _FormatName[14:17],
0: _FormatName[0:4], // jpeg
1: _FormatName[4:7], // png
2: _FormatName[7:10], // gif
3: _FormatName[10:14], // tiff
4: _FormatName[14:17], // bmp
5: _FormatName[17:21], // heic
}
// String implements the Stringer interface.
@@ -40,11 +43,12 @@ func (x Format) String() string {
}
var _FormatValue = map[string]Format{
_FormatName[0:4]: 0,
_FormatName[4:7]: 1,
_FormatName[7:10]: 2,
_FormatName[10:14]: 3,
_FormatName[14:17]: 4,
_FormatName[0:4]: 0, // jpeg
_FormatName[4:7]: 1, // png
_FormatName[7:10]: 2, // gif
_FormatName[10:14]: 3, // tiff
_FormatName[14:17]: 4, // bmp
_FormatName[17:21]: 5, // heic
}
// ParseFormat attempts to convert a string to a Format
+32 -6
View File
@@ -70,11 +70,11 @@ func NewPreviewGenerator(concurrencyLimit int, ffmpegPath string, cacheDir strin
logger.Fatalf("the configured ffmpeg path is not a valid ffprobe binary %s, err: %v", ffmpegPath, err)
}
if errprobe == nil && err == nil {
logger.Infof("Media Enabled : %v", errprobe == nil)
logger.Debugf("Media Enabled : %v", errprobe == nil)
settings.Config.Integrations.Media.FfmpegPath = filepath.Base(ffmpegMainPath)
}
settings.Config.Server.MuPdfAvailable = docEnabled()
logger.Infof("MuPDF Enabled : %v", settings.Config.Server.MuPdfAvailable)
logger.Debugf("MuPDF Enabled : %v", settings.Config.Server.MuPdfAvailable)
return &Service{
sem: make(chan struct{}, concurrencyLimit),
ffmpegPath: ffmpegMainPath,
@@ -126,10 +126,34 @@ func GeneratePreview(file iteminfo.ExtendedFileInfo, previewSize, officeUrl stri
return nil, fmt.Errorf("failed to create image for office file: %w", err)
}
} else if strings.HasPrefix(file.Type, "image") {
// get image bytes from file
imageBytes, err = os.ReadFile(file.RealPath)
if err != nil {
return nil, fmt.Errorf("failed to read image file: %w", err)
logger.Infof("🔍 PREVIEW DEBUG: Image file detected - Name: %s, Type: %s, Extension: %s", file.Name, file.Type, filepath.Ext(file.Name))
// Quick HEIC identification and delegation
if isHEICFile(file) {
logger.Infof("🔄 HEIC DEBUG: HEIC file detected for %s", filepath.Base(file.Name))
// HEIC files need FFmpeg conversion to JPEG with proper size/quality handling
imageBytes, err = service.processHEICFile(file, previewSize)
if err != nil {
logger.Errorf("❌ HEIC DEBUG: HEIC processing failed: %v", err)
return nil, fmt.Errorf("failed to process HEIC image file: %w", err)
}
logger.Infof("✅ HEIC DEBUG: HEIC processing successful, got %d bytes", len(imageBytes))
// For HEIC files, we've already done the resize/conversion, so cache and return directly
cacheKey := CacheKey(file.RealPath, previewSize, file.ModTime, seekPercentage)
if err = service.fileCache.Store(context.Background(), cacheKey, imageBytes); err != nil {
logger.Errorf("failed to cache HEIC image: %v", err)
}
return imageBytes, nil
} else {
// get image bytes from file (non-HEIC images)
logger.Infof("📖 PREVIEW DEBUG: Reading non-HEIC image file directly")
imageBytes, err = os.ReadFile(file.RealPath)
if err != nil {
return nil, fmt.Errorf("failed to read image file: %w", err)
}
}
} else if strings.HasPrefix(file.Type, "video") {
if seekPercentage == 0 {
@@ -223,6 +247,8 @@ func AvailablePreview(file iteminfo.ExtendedFileInfo) bool {
switch ext {
case ".jpg", ".jpeg", ".png", ".bmp", ".tiff":
return true
case ".heic", ".heif":
return service.ffmpegPath != "" // HEIC support available when FFmpeg is available
}
return file.OnlyOfficeId != ""
}
-8
View File
@@ -281,14 +281,6 @@ export default {
},
},
watch: {
currentPrompt: {
handler(prompt) {
if (prompt && prompt.name === "ContextMenu") {
this.setPositions();
}
},
deep: true,
},
hasOverflowItems: {
handler(hasItems) {
mutations.setContextMenuHasItems(hasItems);
+3 -3
View File
@@ -31,7 +31,7 @@ import {
baseURL,
} from "@/utils/constants";
import { getTypeInfo } from "@/utils/mimetype";
import { mutations, state } from "@/store";
import { mutations, state, getters } from "@/store";
import { shareInfo } from "@/utils/constants";
// NEW: Define placeholder and error image URLs for easy configuration
@@ -126,10 +126,10 @@ export default {
return this.imageTargetSrc;
},
showLargeIcon() {
return state.user.viewMode === "gallery" && state.user.preview.highQuality;
return getters.viewMode() === "gallery" && state.user.preview.highQuality;
},
showLarger() {
return state.user.viewMode === "gallery" || state.user.viewMode === "normal";
return getters.viewMode() === "gallery" || getters.viewMode() === "normal";
},
hasMotion() {
return (
@@ -109,7 +109,7 @@ export default {
],
computed: {
galleryView() {
return state.user.viewMode === "gallery";
return getters.viewMode() === "gallery";
},
quickDownloadEnabled() {
// @ts-ignore
+18 -1
View File
@@ -32,9 +32,26 @@ export const getters = {
}
return true
},
displayPreference: () => {
let source = state.sources.current;
if (getters.isShare()) {
source = getters.currentHash();
}
const path = state.route.path;
if (state.displayPreferences?.[source]?.[path]) {
return state.displayPreferences[source][path];
}
return null;
},
viewMode: () => {
return getters.displayPreference()?.viewMode || state.user.viewMode;
},
sorting: () => {
return getters.displayPreference()?.sorting || state.user.sorting;
},
previewType: () => getTypeInfo(state.req.type).simpleType,
isCardView: () =>
(state.user.viewMode == 'gallery' || state.user.viewMode == 'normal') &&
(getters.viewMode() == 'gallery' || getters.viewMode() == 'normal') &&
getters.currentView() == 'listingView',
currentHash: () => shareInfo.hash,
isMobile: () => state.isMobile,
+56 -5
View File
@@ -1,5 +1,6 @@
import * as i18n from "@/i18n";
import { state } from "./state.js";
import { getters } from "./getters.js";
import { emitStateChanged } from './eventBus'; // Import the function from eventBus.js
import { usersApi } from "@/api";
import { notify } from "@/notify";
@@ -241,7 +242,7 @@ export const mutations = {
emitStateChanged();
return;
}
if (value.username != "anonymous") {
if (value.username !== "anonymous") {
mutations.setSources(value);
}
// Ensure locale exists and is valid
@@ -251,8 +252,14 @@ export const mutations = {
i18n.setLocale(value.locale);
}
state.user = value;
state.user.sorting = {};
state.user.sorting.by = "name";
state.user.sorting.asc = true;
// Load display preferences for the current user
const allPreferences = JSON.parse(localStorage.getItem("displayPreferences") || "{}");
state.displayPreferences = allPreferences[state.user.username] || {};
} catch (error) {
console.log(error);
}
@@ -377,8 +384,9 @@ export const mutations = {
}
let sortby = "name"
let asc = true
sortby = state.user.sorting.by;
asc = state.user.sorting.asc;
const sorting = getters.sorting();
sortby = sorting.by;
asc = sorting.asc;
// Separate directories and files
const dirs = value.items.filter((item) => item.type === 'directory');
const files = value.items.filter((item) => item.type !== 'directory');
@@ -404,8 +412,7 @@ export const mutations = {
if (!state.user.sorting) {
state.user.sorting = {};
}
state.user.sorting.by = field;
state.user.sorting.asc = asc;
mutations.updateDisplayPreferences({ sorting: { by: field, asc: asc } });
emitStateChanged();
},
updateListingItems: () => {
@@ -461,4 +468,48 @@ export const mutations = {
state.user.fileLoading.maxConcurrentUpload = value;
emitStateChanged();
},
updateViewModeHistory: ({ source, path, viewMode }) => {
if (!source || !path) return;
if (!state.viewModeHistory) {
state.viewModeHistory = {};
}
if (!state.viewModeHistory[source]) {
state.viewModeHistory[source] = {};
}
state.viewModeHistory[source][path] = viewMode;
localStorage.setItem("viewModeHistory", JSON.stringify(state.viewModeHistory));
emitStateChanged();
},
updateDisplayPreferences: (payload) => {
let source = state.sources.current;
if (getters.isShare()) {
source = getters.currentHash();
}
const path = state.route.path;
if (!source || !path) return;
if (!state.displayPreferences) {
state.displayPreferences = {};
}
if (!state.displayPreferences[source]) {
state.displayPreferences[source] = {};
}
if (!state.displayPreferences[source][path]) {
state.displayPreferences[source][path] = {};
}
state.displayPreferences[source][path] = {
...state.displayPreferences[source][path],
...payload,
};
const allPreferences = JSON.parse(localStorage.getItem("displayPreferences") || "{}");
if (!allPreferences[state.user.username]) {
allPreferences[state.user.username] = {};
}
allPreferences[state.user.username] = state.displayPreferences;
localStorage.setItem("displayPreferences", JSON.stringify(allPreferences));
emitStateChanged();
},
};
+1
View File
@@ -24,6 +24,7 @@ export const state = reactive({
isMobile: window.innerWidth <= 800,
isSearchActive: false,
showSidebar: false,
displayPreferences: {},
usages: {},
editor: null,
serverHasMultipleSources: false,
+5 -3
View File
@@ -92,7 +92,7 @@ export default {
normal: "view_module",
gallery: "grid_view",
};
return icons[state.user.viewMode] || "grid_view";
return icons[getters.viewMode()] || "grid_view";
},
isShare() {
return getters.isShare();
@@ -157,9 +157,11 @@ export default {
},
switchView() {
mutations.closeHovers();
const index = this.viewModes.indexOf(state.user.viewMode);
const index = this.viewModes.indexOf(getters.viewMode());
const next = (index + 1) % this.viewModes.length;
mutations.updateCurrentUser({ viewMode: this.viewModes[next] });
const newViewMode = this.viewModes[next];
mutations.updateDisplayPreferences({ viewMode: newViewMode });
mutations.updateCurrentUser({ viewMode: newViewMode });
},
multiAction() {
const listingView = getters.currentView();
+7 -7
View File
@@ -281,16 +281,16 @@ export default {
return state.multiple;
},
nameSorted() {
return state.user.sorting.by === "name";
return getters.sorting().by === "name";
},
sizeSorted() {
return state.user.sorting.by === "size";
return getters.sorting().by === "size";
},
modifiedSorted() {
return state.user.sorting.by === "modified";
return getters.sorting().by === "modified";
},
ascOrdered() {
return state.user.sorting.asc;
return getters.sorting().asc;
},
items() {
return getters.reqItems();
@@ -335,11 +335,11 @@ export default {
normal: "grid_view",
gallery: "view_list",
};
return icons[state.user.viewMode];
return icons[getters.viewMode()];
},
listingViewMode() {
this.colunmsResize();
return state.user.viewMode;
return getters.viewMode();
},
selectedCount() {
return state.selected.length;
@@ -843,7 +843,7 @@ export default {
`calc(${100 / this.numColumns}% - 1em)`
);
if (state.user.viewMode == "gallery") {
if (getters.viewMode() == "gallery") {
document.documentElement.style.setProperty(
"--item-height",
`calc(${this.columnWidth / 20}em)`
+37 -5
View File
@@ -104,6 +104,36 @@ export default {
}
},
methods: {
getInternalUrlInfo() {
if (this.clientConfig && this.clientConfig.document && this.clientConfig.document.url) {
try {
const docUrlOrigin = new URL(this.clientConfig.document.url).origin;
const windowOrigin = window.location.origin;
if (docUrlOrigin !== windowOrigin) {
return {
isSet: true,
message: `${docUrlOrigin}`
};
} else {
return {
isSet: false,
message: "Not set, using window.location"
};
}
} catch (e) {
return {
isSet: false,
message: "⚠️ Error parsing document URL"
};
}
}
return {
isSet: false,
message: "Analyzing..."
};
},
close() {
const current = window.location.pathname;
const newpath = removeLastDir(current);
@@ -137,7 +167,7 @@ export default {
<div style="margin-bottom: 15px; padding: 10px; background: #f5f5f5; border-radius: 4px;">
<strong>Configuration:</strong><br/>
OnlyOffice URL: ${this.onlyOfficeUrl}<br/>
Internal URL: Check server logs<br/>
Internal URL: ${this.getInternalUrlInfo().message}<br/>
Base URL: ${baseURL}<br/>
Source: ${this.source}<br/>
Path: ${this.path}<br/>
@@ -175,6 +205,7 @@ export default {
const filename = this.path.split('/').pop() || "";
const isShare = getters.isShare();
const internalUrlInfo = this.getInternalUrlInfo();
// Build config details if available
let configDetailsHtml = "";
@@ -184,7 +215,6 @@ export default {
// Extract domains for network flow analysis
const downloadDomain = doc.url ? new URL(doc.url).origin : 'N/A';
const callbackDomain = editor && editor.callbackUrl ? new URL(editor.callbackUrl).origin : 'N/A';
configDetailsHtml = `
<div style="margin-bottom: 15px; padding: 10px; background: #e3f2fd; border-radius: 4px;">
@@ -196,9 +226,8 @@ export default {
Callback URL: ${editor && editor.callbackUrl ? editor.callbackUrl.substring(0, 80) + '...' : 'N/A'}<br/>
<br/>
<strong>Network Flow:</strong><br/>
Browser ↔ OnlyOffice: ${this.onlyOfficeUrl}<br/>
Browser (${window.location.origin}) ↔ OnlyOffice: ${this.onlyOfficeUrl}<br/>
OnlyOffice → FileBrowser: ${downloadDomain}<br/>
OnlyOffice → FileBrowser: ${callbackDomain}<br/>
</div>
`;
}
@@ -234,7 +263,7 @@ export default {
<div style="margin-bottom: 15px; padding: 10px; background: #f5f5f5; border-radius: 4px;">
<strong>Basic Configuration:</strong><br/>
OnlyOffice URL: ${this.onlyOfficeUrl}<br/>
Internal URL: Check server logs<br/>
Internal URL: ${internalUrlInfo.message}<br/>
Base URL: ${baseURL}<br/>
Source: ${this.source}<br/>
Path: ${this.path}<br/>
@@ -372,6 +401,9 @@ export default {
errorMsg += "<strong>Error Code -4:</strong> Document download failed<br/>";
errorMsg += "• Authentication may have expired<br/>";
errorMsg += "• Check download URL accessibility<br/>";
if (!this.getInternalUrlInfo().isSet) {
errorMsg += "• <strong>Suggestion:</strong> consider adding `server.internalUrl` if your OnlyOffice service is on the same network as FileBrowser<br/>";
}
} else {
errorMsg += `<strong>Error Code:</strong> ${errorInfo || 'Unknown error'}<br/>`;
errorMsg += "• Check browser console for detailed error messages<br/>";
+3 -2
View File
@@ -207,7 +207,7 @@ import { state, getters, mutations } from "@/store";
import { getFileExtension } from "@/utils/files";
import { convertToVTT } from "@/utils/subtitles";
import { getTypeInfo } from "@/utils/mimetype";
import { muPdfAvailable } from "@/utils/constants";
import { mediaAvailable, muPdfAvailable } from "@/utils/constants";
import { shareInfo } from "@/utils/constants";
import jsmediatags from "jsmediatags/dist/jsmediatags.min.js";
@@ -317,7 +317,8 @@ export default {
return getters.previewType();
},
raw() {
if (this.pdfConvertable) {
const showFullSizeHeic = state.req.type === "image/heic" && !state.isSafari && mediaAvailable;
if (this.pdfConvertable || showFullSizeHeic) {
if (getters.isShare()) {
const previewPath = url.removeTrailingSlash(state.req.path);
return publicApi.getPreviewURL(previewPath, "original");
-1
View File
@@ -19,7 +19,6 @@ async function globalSetup() {
await page.locator('button[aria-label="File-Actions"]').click();
await page.locator('button[aria-label="Share"]').click();
await page.locator('button[aria-label="Share-Confirm"]').click();
await page.waitForResponse(response => response.url().includes('/api/share') && response.status() === 200);
await expect(page.locator("div[aria-label='share-prompt'] .card-content table tbody tr:not(:has(th))")).toHaveCount(1);
const shareHash = await page.locator("div[aria-label='share-prompt'] .card-content table tbody tr:not(:has(th)) td").first().textContent();
if (!shareHash) {