Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8a95cb3cf | ||
|
|
5e79faefa1 | ||
|
|
92909a7403 | ||
|
|
2385994736 | ||
|
|
b0df91cd45 | ||
|
|
3330468c1b | ||
|
|
8ef0435602 | ||
|
|
32cbec011a | ||
|
|
e6a8d090ad | ||
|
|
527d1ec810 | ||
|
|
586a76c5ca | ||
|
|
5be126dc47 | ||
|
|
b8d3b6aadc | ||
|
|
2d2a3d7707 | ||
|
|
7baf2653e0 | ||
|
|
256f4ca0b8 |
No files matched your search
@@ -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
@@ -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/)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 != ""
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
@@ -24,6 +24,7 @@ export const state = reactive({
|
||||
isMobile: window.innerWidth <= 800,
|
||||
isSearchActive: false,
|
||||
showSidebar: false,
|
||||
displayPreferences: {},
|
||||
usages: {},
|
||||
editor: null,
|
||||
serverHasMultipleSources: false,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)`
|
||||
|
||||
@@ -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/>";
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user