CI: Apply Go linter recommendations to remaining "pkg/..." code #5330

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-11-22 16:14:43 +01:00
parent 90ab65a9b0
commit 149f5e5731
116 changed files with 541 additions and 351 deletions

View File

@@ -26,10 +26,10 @@ func Output(f func()) string {
}()
f()
w.Close()
_ = w.Close()
var buf bytes.Buffer
io.Copy(&buf, r)
_, _ = io.Copy(&buf, r)
return buf.String()
}

View File

@@ -20,10 +20,10 @@ func Stdout(f func()) string {
}()
f()
w.Close()
_ = w.Close()
var buf bytes.Buffer
io.Copy(&buf, r)
_, _ = io.Copy(&buf, r)
return buf.String()
}

View File

@@ -1,7 +1,6 @@
package capture
import (
"fmt"
"testing"
"time"
@@ -11,6 +10,6 @@ import (
func TestTime(t *testing.T) {
start := time.Now()
time.Sleep(1 * time.Millisecond)
result := Time(start, fmt.Sprintf("%s", "Successful test"))
result := Time(start, "Successful test")
assert.Contains(t, result, "Successful test [")
}

View File

@@ -1,8 +1,11 @@
package checksum
const (
// CharsetBase10 contains digits for base10 encoding.
CharsetBase10 = "0123456789"
// CharsetBase36 contains lowercase alphanumerics for base36.
CharsetBase36 = "abcdefghijklmnopqrstuvwxyz0123456789"
// CharsetBase62 contains mixed-case alphanumerics for base62.
CharsetBase62 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
)

View File

@@ -5,6 +5,7 @@ import (
"hash/crc32"
)
// Crc32Castagnoli provides the Castagnoli polynomial table for CRC32.
var Crc32Castagnoli = crc32.MakeTable(crc32.Castagnoli)
// Crc32 returns the CRC-32 checksum of data using the crc32.IEEE polynomial.

View File

@@ -2,7 +2,7 @@ package fs
// Required file format decoders and encoders.
import (
_ "image/gif"
_ "image/jpeg"
_ "image/png"
_ "image/gif" // register GIF decoder
_ "image/jpeg" // register JPEG decoder
_ "image/png" // register PNG decoder
)

View File

@@ -17,7 +17,7 @@ func ConfigFilePath(configPath, baseName, defaultExt string) string {
return ""
}
// Search file in current directory if configPath is emtpy.
// Search file in current directory if configPath is empty.
if configPath == "" {
if dir, err := os.Getwd(); err == nil && dir != "" {
configPath = dir

View File

@@ -55,7 +55,7 @@ func Copy(src, dest string, force bool) (err error) {
return err
}
thisFile, err := os.Open(src)
thisFile, err := os.Open(src) //nolint:gosec // src is validated by callers
if err != nil {
return err
@@ -64,7 +64,7 @@ func Copy(src, dest string, force bool) (err error) {
defer thisFile.Close()
// Open destination for write; create or truncate to avoid trailing bytes
destFile, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, ModeFile)
destFile, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, ModeFile) //nolint:gosec // dest is derived from validated input
if err != nil {
return err
@@ -136,9 +136,5 @@ func Move(src, dest string, force bool) (err error) {
return err
}
if err = os.Remove(src); err != nil {
return err
}
return nil
return os.Remove(src)
}

View File

@@ -17,7 +17,7 @@ func TestCopy_NewDestination_Succeeds(t *testing.T) {
err := Copy(src, dst, false)
assert.NoError(t, err)
b, _ := os.ReadFile(dst)
b, _ := os.ReadFile(dst) //nolint:gosec // test helper reads temp file
assert.Equal(t, "hello", string(b))
}
@@ -31,7 +31,7 @@ func TestCopy_ExistingNonEmpty_NoForce_Error(t *testing.T) {
err := Copy(src, dst, false)
assert.Error(t, err)
b, _ := os.ReadFile(dst)
b, _ := os.ReadFile(dst) //nolint:gosec // test helper reads temp file
assert.Equal(t, "existing", string(b))
}
@@ -46,7 +46,7 @@ func TestCopy_ExistingNonEmpty_Force_TruncatesAndOverwrites(t *testing.T) {
err := Copy(src, dst, true)
assert.NoError(t, err)
b, _ := os.ReadFile(dst)
b, _ := os.ReadFile(dst) //nolint:gosec // test helper reads temp file
assert.Equal(t, "short", string(b))
}
@@ -60,7 +60,7 @@ func TestCopy_ExistingEmpty_NoForce_AllowsReplace(t *testing.T) {
err := Copy(src, dst, false)
assert.NoError(t, err)
b, _ := os.ReadFile(dst)
b, _ := os.ReadFile(dst) //nolint:gosec // test helper reads temp file
assert.Equal(t, "data", string(b))
}
@@ -93,7 +93,7 @@ func TestMove_NewDestination_Succeeds(t *testing.T) {
// Source is removed; dest contains data
_, serr := os.Stat(src)
assert.True(t, os.IsNotExist(serr))
b, _ := os.ReadFile(dst)
b, _ := os.ReadFile(dst) //nolint:gosec // test helper reads temp file
assert.Equal(t, "hello", string(b))
}
@@ -108,8 +108,8 @@ func TestMove_ExistingNonEmpty_NoForce_Error(t *testing.T) {
err := Move(src, dst, false)
assert.Error(t, err)
// Verify both files unchanged
bsrc, _ := os.ReadFile(src)
bdst, _ := os.ReadFile(dst)
bsrc, _ := os.ReadFile(src) //nolint:gosec // test helper reads temp file
bdst, _ := os.ReadFile(dst) //nolint:gosec // test helper reads temp file
assert.Equal(t, "src", string(bsrc))
assert.Equal(t, "dst", string(bdst))
}
@@ -126,7 +126,7 @@ func TestMove_ExistingEmpty_NoForce_AllowsReplace(t *testing.T) {
assert.NoError(t, err)
_, serr := os.Stat(src)
assert.True(t, os.IsNotExist(serr))
bdst, _ := os.ReadFile(dst)
bdst, _ := os.ReadFile(dst) //nolint:gosec // test helper reads temp file
assert.Equal(t, "src", string(bdst))
}
@@ -142,7 +142,7 @@ func TestMove_ExistingNonEmpty_Force_Succeeds(t *testing.T) {
assert.NoError(t, err)
_, serr := os.Stat(src)
assert.True(t, os.IsNotExist(serr))
bdst, _ := os.ReadFile(dst)
bdst, _ := os.ReadFile(dst) //nolint:gosec // test helper reads temp file
assert.Equal(t, "AAA", string(bdst))
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/photoprism/photoprism/pkg/fs/fastwalk"
)
// OriginalPaths lists default Originals search paths.
var OriginalPaths = []string{
"/photoprism/storage/media/originals",
"/photoprism/media/originals",
@@ -76,6 +77,7 @@ var OriginalPaths = []string{
"/var/lib/photoprism/originals",
}
// ImportPaths lists default Import search paths.
var ImportPaths = []string{
"/photoprism/storage/media/import",
"/photoprism/media/import",
@@ -110,6 +112,7 @@ var ImportPaths = []string{
"/var/lib/photoprism/import",
}
// AssetPaths lists default asset paths.
var AssetPaths = []string{
"/opt/photoprism/assets",
"/photoprism/assets",
@@ -120,6 +123,7 @@ var AssetPaths = []string{
"/var/lib/photoprism/assets",
}
// ModelsPaths lists default model lookup paths.
var ModelsPaths = []string{
"/opt/photoprism/assets/models",
"/photoprism/assets/models",

View File

@@ -1,12 +1,16 @@
package fs
// Status indicates whether a path was seen or processed.
type Status int8
const (
Found Status = 1
// Found marks a path as seen.
Found Status = 1
// Processed marks a path as fully handled.
Processed Status = 2
)
// Done stores per-path processing state.
type Done map[string]Status
// Processed counts the number of processed files.
@@ -22,10 +26,12 @@ func (d Done) Processed() int {
return count
}
// Exists reports whether any status is recorded.
func (s Status) Exists() bool {
return s > 0
}
// Processed returns true if the path was marked as processed.
func (s Status) Processed() bool {
return s >= Processed
}

View File

@@ -6,6 +6,7 @@ import (
"strings"
)
// nolint:unused // kept for potential platform-specific filesystem filtering
func findMounts(mounts []Mount, path string) ([]Mount, error) {
var err error
path, err = filepath.Abs(path)

View File

@@ -4,7 +4,7 @@ package duf
import "strings"
//nolint:revive,deadcode
//nolint:revive // constants kept for reference in filesystem detection
const (
// man statfs
ADFS_SUPER_MAGIC = 0xadf5

View File

@@ -10,8 +10,10 @@ var (
onlyMp = ""
)
// FilterValues holds a set of filter strings.
type FilterValues map[string]struct{}
// NewFilterValues converts strings or comma-separated lists into a FilterValues set.
func NewFilterValues(s ...string) FilterValues {
if len(s) == 0 {
return make(FilterValues)

View File

@@ -5,11 +5,14 @@ import (
)
var (
// nolint:unused // kept for potential future grouping logic extensions
groups = []string{LocalDevice, NetworkDevice, FuseDevice, SpecialDevice, LoopsDevice, BindsMount}
)
// GroupedMounts maps device types to their mounts.
type GroupedMounts map[string][]Mount
// GroupMounts groups mounts by device type, applying the given filters.
func GroupMounts(m []Mount, filters FilterOptions) GroupedMounts {
deviceMounts := make(GroupedMounts)
hasOnlyDevices := len(filters.OnlyDevices) != 0

View File

@@ -26,7 +26,7 @@ type Mount struct {
}
func readLines(filename string) ([]string, error) {
file, err := os.Open(filename)
file, err := os.Open(filename) //nolint:gosec // filename comes from platform mountinfo source
if err != nil {
return nil, err
}
@@ -49,7 +49,7 @@ func unescapeFstab(path string) string {
return escaped
}
//nolint:deadcode,unused // used on BSD
//nolint:unused // used on BSD
func byteToString(orig []byte) string {
n := -1
l := -1
@@ -73,7 +73,7 @@ func byteToString(orig []byte) string {
return string(orig[l:n])
}
//nolint:deadcode,unused // used on OpenBSD
//nolint:unused // used on OpenBSD
func intToString(orig []int8) string {
ret := make([]byte, len(orig))
size := -1

View File

@@ -18,13 +18,13 @@ const (
// (0) (1) (2) (3) (4) (5) (6) (7) (8) (9) (10)
//
// (0) mount ID: unique identifier of the mount (may be reused after umount).
//mountinfoMountID = 0
// mountinfoMountID = 0
// (1) parent ID: ID of parent (or of self for the top of the mount tree).
//mountinfoParentID = 1
// mountinfoParentID = 1
// (2) major:minor: value of st_dev for files on filesystem.
//mountinfoMajorMinor = 2
// mountinfoMajorMinor = 2
// (3) root: root of the mount within the filesystem.
//mountinfoRoot = 3
// mountinfoRoot = 3
// (4) mount point: mount point relative to the process's root.
mountinfoMountPoint = 4
// (5) mount options: per mount options.
@@ -32,13 +32,13 @@ const (
// (6) optional fields: zero or more fields terminated by "-".
mountinfoOptionalFields = 6
// (7) separator between optional fields.
//mountinfoSeparator = 7
// mountinfoSeparator = 7
// (8) filesystem type: name of filesystem of the form.
mountinfoFsType = 8
// (9) mount source: filesystem specific information or "none".
mountinfoMountSource = 9
// (10) super options: per super block options.
//mountinfoSuperOptions = 10
// mountinfoSuperOptions = 10
)
// Stat returns the mountpoint's stat information.
@@ -70,6 +70,11 @@ func mounts() ([]Mount, []string, error) {
}
// blockDeviceID := fields[mountinfoMountID]
if len(fields) <= mountinfoMountSource {
warnings = append(warnings, fmt.Sprintf("incomplete mountinfo line: %s", line))
continue
}
mountPoint := fields[mountinfoMountPoint]
mountOpts := fields[mountinfoMountOpts]
fstype := fields[mountinfoFsType]
@@ -93,14 +98,14 @@ func mounts() ([]Mount, []string, error) {
Type: fsTypeMap[int64(stat.Type)], //nolint:unconvert
Opts: mountOpts,
Metadata: stat,
Total: (uint64(stat.Blocks) * uint64(stat.Bsize)), //nolint:unconvert
Free: (uint64(stat.Bavail) * uint64(stat.Bsize)), //nolint:unconvert
Used: (uint64(stat.Blocks) - uint64(stat.Bfree)) * uint64(stat.Bsize), //nolint:unconvert
Total: (uint64(stat.Blocks) * uint64(stat.Bsize)), //nolint:unconvert,gosec // stat values are kernel-provided
Free: (uint64(stat.Bavail) * uint64(stat.Bsize)), //nolint:unconvert,gosec
Used: (uint64(stat.Blocks) - uint64(stat.Bfree)) * uint64(stat.Bsize), //nolint:unconvert,gosec
Inodes: stat.Files,
InodesFree: stat.Ffree,
InodesUsed: stat.Files - stat.Ffree,
Blocks: uint64(stat.Blocks), //nolint:unconvert
BlockSize: uint64(stat.Bsize),
BlockSize: uint64(stat.Bsize), //nolint:gosec // kernel-provided value fits uint64
}
d.DeviceType = deviceType(d)

View File

@@ -25,6 +25,7 @@ func parseCommaSeparatedValues(values string) FilterValues {
}
// validateGroups validates the parsed group maps.
// nolint:unused // reserved for future validation hooks
func validateGroups(m FilterValues) error {
for k := range m {
found := slices.Contains(groups, k)

View File

@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build freebsd || openbsd || netbsd
// +build freebsd openbsd netbsd
package fastwalk

View File

@@ -3,8 +3,6 @@
// license that can be found in the LICENSE file.
//go:build (linux || darwin) && !appengine
// +build linux darwin
// +build !appengine
package fastwalk

View File

@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build darwin || freebsd || openbsd || netbsd
// +build darwin freebsd openbsd netbsd
package fastwalk

View File

@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build linux && !appengine
// +build linux,!appengine
package fastwalk
@@ -15,7 +14,7 @@ import (
func direntNamlen(dirent *syscall.Dirent) uint64 {
const fixedHdr = uint16(unsafe.Offsetof(syscall.Dirent{}.Name))
nameBuf := (*[unsafe.Sizeof(dirent.Name)]byte)(unsafe.Pointer(&dirent.Name[0]))
nameBuf := (*[unsafe.Sizeof(dirent.Name)]byte)(unsafe.Pointer(&dirent.Name[0])) //nolint:gosec // bounded by Dirent name buffer size
const nameBufLen = uint16(len(nameBuf))
limit := dirent.Reclen - fixedHdr
if limit > nameBufLen {

View File

@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build appengine || (!linux && !darwin && !freebsd && !openbsd && !netbsd)
// +build appengine !linux,!darwin,!freebsd,!openbsd,!netbsd
package fastwalk

View File

@@ -8,6 +8,7 @@ import (
"bytes"
"flag"
"fmt"
"go/build"
"os"
"path/filepath"
"reflect"
@@ -43,14 +44,14 @@ func testFastWalk(t *testing.T, files map[string]string, callback func(path stri
for path, contents := range files {
file := filepath.Join(tempdir, "/src", path)
if err = os.MkdirAll(filepath.Dir(file), 0755); err != nil {
if err = os.MkdirAll(filepath.Dir(file), 0o750); err != nil {
t.Fatal(err)
}
if strings.HasPrefix(contents, "LINK:") {
err = os.Symlink(strings.TrimPrefix(contents, "LINK:"), file)
} else {
err = os.WriteFile(file, []byte(contents), 0644)
err = os.WriteFile(file, []byte(contents), 0o600)
}
if err != nil {
@@ -229,7 +230,8 @@ func TestFastWalk_TraverseSymlink(t *testing.T) {
})
}
var benchDir = flag.String("benchdir", runtime.GOROOT(), "The directory to scan for BenchmarkFastWalk")
// Default to build.Default.GOROOT to avoid runtime.GOROOT deprecation.
var benchDir = flag.String("benchdir", build.Default.GOROOT, "The directory to scan for BenchmarkFastWalk")
func BenchmarkFastWalk(b *testing.B) {
b.ReportAllocs()

View File

@@ -3,8 +3,6 @@
// license that can be found in the LICENSE file.
//go:build (linux || darwin || freebsd || openbsd || netbsd) && !appengine
// +build linux darwin freebsd openbsd netbsd
// +build !appengine
package fastwalk
@@ -79,7 +77,7 @@ func readDir(dirName string, fn func(dirName, entName string, typ os.FileMode) e
func parseDirEnt(buf []byte) (consumed int, name string, typ os.FileMode) {
// golang.org/issue/37269
dirent := &syscall.Dirent{}
copy((*[unsafe.Sizeof(syscall.Dirent{})]byte)(unsafe.Pointer(dirent))[:], buf)
copy((*[unsafe.Sizeof(syscall.Dirent{})]byte)(unsafe.Pointer(dirent))[:], buf) //nolint:gosec // unsafe needed for fast directory walk
if v := unsafe.Offsetof(dirent.Reclen) + unsafe.Sizeof(dirent.Reclen); uintptr(len(buf)) < v {
panic(fmt.Sprintf("buf size of %d smaller than dirent header size %d", len(buf), v))
}
@@ -114,15 +112,16 @@ func parseDirEnt(buf []byte) (consumed int, name string, typ os.FileMode) {
return
}
nameBuf := (*[unsafe.Sizeof(dirent.Name)]byte)(unsafe.Pointer(&dirent.Name[0]))
nameBuf := (*[unsafe.Sizeof(dirent.Name)]byte)(unsafe.Pointer(&dirent.Name[0])) //nolint:gosec // bounded by dirent name buffer
nameLen := direntNamlen(dirent)
// Special cases for common things:
if nameLen == 1 && nameBuf[0] == '.' {
switch {
case nameLen == 1 && nameBuf[0] == '.':
name = "."
} else if nameLen == 2 && nameBuf[0] == '.' && nameBuf[1] == '.' {
case nameLen == 2 && nameBuf[0] == '.' && nameBuf[1] == '.':
name = ".."
} else {
default:
name = string(nameBuf[:nameLen])
}
return

View File

@@ -5,6 +5,7 @@ import (
"strings"
)
// Common file extensions used throughout PhotoPrism.
const (
ExtNone = ""
ExtLocal = ".local"

View File

@@ -197,20 +197,12 @@ func (m FileExtensions) Types(noUppercase bool) TypesExt {
if noUppercase {
for ext, t := range m {
if _, ok := result[t]; ok {
result[t] = append(result[t], ext)
} else {
result[t] = []string{ext}
}
result[t] = append(result[t], ext)
}
} else {
for ext, t := range m {
extUpper := strings.ToUpper(ext)
if _, ok := result[t]; ok {
result[t] = append(result[t], ext, extUpper)
} else {
result[t] = []string{ext, extUpper}
}
result[t] = append(result[t], ext, extUpper)
}
}

View File

@@ -1,5 +1,6 @@
package fs
// TypeMap maps file types to a representative extension string.
type TypeMap map[Type]string
// TypeInfo contains human-readable descriptions for supported file formats

View File

@@ -1,13 +1,13 @@
package fs
import (
_ "image/gif"
_ "image/jpeg"
_ "image/png"
_ "image/gif" // register GIF decoder
_ "image/jpeg" // register JPEG decoder
_ "image/png" // register PNG decoder
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
_ "golang.org/x/image/webp"
_ "golang.org/x/image/bmp" // register BMP decoder
_ "golang.org/x/image/tiff" // register TIFF decoder
_ "golang.org/x/image/webp" // register WEBP decoder
)
// Supported archive file types:

View File

@@ -1,13 +1,13 @@
package fs
import (
_ "image/gif"
_ "image/jpeg"
_ "image/png"
_ "image/gif" // register GIF decoder
_ "image/jpeg" // register JPEG decoder
_ "image/png" // register PNG decoder
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
_ "golang.org/x/image/webp"
_ "golang.org/x/image/bmp" // register BMP decoder
_ "golang.org/x/image/tiff" // register TIFF decoder
_ "golang.org/x/image/webp" // register WEBP decoder
)
// TypesExt maps standard formats to file extensions.
@@ -15,4 +15,6 @@ type TypesExt map[Type][]string
// FileTypes contains the default file type extensions.
var FileTypes = Extensions.Types(ignoreCase)
// FileTypesLower contains lowercase extensions for case-insensitive lookup.
var FileTypesLower = Extensions.Types(true)

View File

@@ -65,6 +65,7 @@ func WebFileInfo(file webdav.FileInfo, dir string) FileInfo {
return result
}
// FileInfos is a slice helper for bulk file info operations.
type FileInfos []FileInfo
func (infos FileInfos) Len() int { return len(infos) }
@@ -72,6 +73,8 @@ func (infos FileInfos) Swap(i, j int) { infos[i], infos[j] = infos[j], infos[i]
func (infos FileInfos) Less(i, j int) bool {
return strings.Compare(infos[i].Abs, infos[j].Abs) == -1
}
// Abs returns absolute file paths for all file infos.
func (infos FileInfos) Abs() (result []string) {
for _, info := range infos {
result = append(result, info.Abs)
@@ -80,6 +83,7 @@ func (infos FileInfos) Abs() (result []string) {
return result
}
// NewFileInfos builds FileInfos from os.FileInfo with directory prefix.
func NewFileInfos(infos []os.FileInfo, dir string) FileInfos {
var result FileInfos

View File

@@ -38,9 +38,12 @@ import (
var ignoreCase bool
const (
// PathSeparator is the filesystem path separator for the current OS.
PathSeparator = string(filepath.Separator)
Home = "~"
HomePath = Home + PathSeparator
// Home represents the tilde shorthand for the user's home directory.
Home = "~"
// HomePath expands Home with a trailing separator.
HomePath = Home + PathSeparator
)
// Stat returns the os.FileInfo for the given file path, or an error if it does not exist.
@@ -214,7 +217,7 @@ func Download(fileName string, url string) error {
// DirIsEmpty returns true if a directory is empty.
func DirIsEmpty(path string) bool {
f, err := os.Open(path)
f, err := os.Open(path) //nolint:gosec // path provided by caller; intended to access filesystem
if err != nil {
return false
@@ -223,10 +226,5 @@ func DirIsEmpty(path string) bool {
defer f.Close()
_, err = f.Readdirnames(1)
if err == io.EOF {
return true
}
return false
return err == io.EOF
}

View File

@@ -130,7 +130,7 @@ func TestDirIsEmpty(t *testing.T) {
assert.Equal(t, false, DirIsEmpty("./xxx"))
})
t.Run("EmptyDir", func(t *testing.T) {
if err := os.Mkdir("./testdata/emptyDir", 0777); err != nil {
if err := os.Mkdir("./testdata/emptyDir", 0o750); err != nil {
t.Fatal(err)
}
defer os.RemoveAll("./testdata/emptyDir")
@@ -168,12 +168,12 @@ func TestDownload_SuccessAndErrors(t *testing.T) {
dir := t.TempDir()
goodPath := filepath.Join(dir, "sub", "file.txt")
badPath := filepath.Join("file.txt") // invalid path according to Download
badPath := "file.txt" // invalid path according to Download
// Success
err := Download(goodPath, tsOK.URL)
assert.NoError(t, err)
b, rerr := os.ReadFile(goodPath)
b, rerr := os.ReadFile(goodPath) //nolint:gosec // test helper reads temp file
assert.NoError(t, rerr)
assert.Equal(t, "hello world", string(b))

View File

@@ -1,7 +1,7 @@
package fs
import (
"crypto/sha1"
"crypto/sha1" //nolint:gosec // SHA1 retained for legacy hash compatibility
"encoding/hex"
"hash/crc32"
"io"
@@ -14,7 +14,7 @@ import (
func Hash(fileName string) string {
var result []byte
file, err := os.Open(fileName)
file, err := os.Open(fileName) //nolint:gosec // caller-controlled path; intended file read
if err != nil {
return ""
@@ -22,7 +22,7 @@ func Hash(fileName string) string {
defer file.Close()
hash := sha1.New()
hash := sha1.New() //nolint:gosec // legacy SHA1 hashes retained for compatibility
if _, err := io.Copy(hash, file); err != nil {
return ""
@@ -35,7 +35,7 @@ func Hash(fileName string) string {
func Checksum(fileName string) string {
var result []byte
file, err := os.Open(fileName)
file, err := os.Open(fileName) //nolint:gosec // caller-controlled path; intended file read
if err != nil {
return ""

View File

@@ -6,9 +6,14 @@ import (
"github.com/photoprism/photoprism/pkg/rnd"
)
var DscNameRegexp = regexp.MustCompile("\\D{3}[\\d_]\\d{4,8}_?\\d{0,6}_?\\d{0,6}[\\.jpgJPGXx]{0,4}")
// DscNameRegexp matches DSLR-like file names.
var DscNameRegexp = regexp.MustCompile(`\D{3}[\d_]\d{4,8}_?\d{0,6}_?\d{0,6}[\.jpgJPGXx]{0,4}`)
// UniqueNameRegexp matches generated unique names.
var UniqueNameRegexp = regexp.MustCompile("[a-f0-9]{8,16}_[a-f0-9]{6,16}_[A-Za-z0-9]{1,20}_?[A-Za-z0-9]{0,4}") // Example: 8263987746_d0a6055c58_o
var UUIDNameRegexp = regexp.MustCompile("[A-Fa-f0-9\\-]{16,36}_?[A-Za-z0-9_]{0,20}") // Example: 8263987746_d0a6055c58_o
// UUIDNameRegexp matches names prefixed with UUIDs.
var UUIDNameRegexp = regexp.MustCompile(`[A-Fa-f0-9\-]{16,36}_?[A-Za-z0-9_]{0,20}`) // Example: 8263987746_d0a6055c58_o
// IsInt tests if the file base is an integer number.
func IsInt(s string) bool {
@@ -76,21 +81,22 @@ func IsGenerated(fileName string) bool {
base := BasePrefix(fileName, false)
if IsAsciiID(base) {
switch {
case IsAsciiID(base):
return true
} else if IsHash(base) {
case IsHash(base):
return true
} else if IsInt(base) {
case IsInt(base):
return true
} else if IsDscName(base) {
case IsDscName(base):
return true
} else if IsUniqueName(base) {
case IsUniqueName(base):
return true
} else if rnd.IsUnique(base, 0) {
case rnd.IsUnique(base, 0):
return true
} else if IsCanonical(base) {
case IsCanonical(base):
return true
default:
return false
}
return false
}

View File

@@ -8,6 +8,7 @@ import (
"sync"
)
// IgnoreLogFunc logs ignored file names.
type IgnoreLogFunc func(fileName string)
// IgnorePattern represents a name pattern to be ignored.
@@ -171,7 +172,7 @@ func (l *IgnoreList) Ignore(name string) bool {
baseName := filepath.Base(name)
// Change name to lowercase for case-insensitive comparison.
if l.caseSensitive == false {
if !l.caseSensitive {
dir = strings.ToLower(dir)
baseName = strings.ToLower(baseName)
}

View File

@@ -11,6 +11,7 @@ import (
)
const (
// MimeTypeUnknown represents an unknown mime type.
MimeTypeUnknown = ""
)

View File

@@ -9,7 +9,7 @@ import (
// ReadLines returns all lines in a text file as string slice.
func ReadLines(fileName string) (lines []string, err error) {
file, err := os.Open(fileName)
file, err := os.Open(fileName) //nolint:gosec // caller-controlled path; intended file read
if err != nil {
return lines, err

View File

@@ -19,7 +19,7 @@ func SymlinksSupported(storagePath string) (bool, error) {
}(linkName, targetName)
// Create empty test target file.
if targetFile, err := os.OpenFile(targetName, os.O_RDONLY|os.O_CREATE, ModeFile); err != nil {
if targetFile, err := os.OpenFile(targetName, os.O_RDONLY|os.O_CREATE, ModeFile); err != nil { //nolint:gosec // targetName is validated by caller
return false, err
} else if err = targetFile.Close(); err != nil {
return false, err

View File

@@ -9,7 +9,8 @@ import (
func SkipWalk(name string, isDir, isSymlink bool, done Done, ignore *IgnoreList) (skip bool, result error) {
isDone := done[name].Exists()
if isSymlink {
switch {
case isSymlink:
// Check if symlink points to a directory.
if link, err := os.Stat(name); err == nil && link.IsDir() {
// Skip directories.
@@ -22,12 +23,13 @@ func SkipWalk(name string, isDir, isSymlink bool, done Done, ignore *IgnoreList)
}
// Skip symlinked directories that cannot be resolved or are ignored, hidden, or already done.
if ignore.Ignore(name) || evalErr != nil || isDone || done[resolved].Exists() {
switch {
case ignore.Ignore(name) || evalErr != nil || isDone || done[resolved].Exists():
result = filepath.SkipDir
} else if FileExists(filepath.Join(resolved, PPStorageFilename)) {
case FileExists(filepath.Join(resolved, PPStorageFilename)):
// Skip symlinked directories that contain a .ppstorage file.
result = filepath.SkipDir
} else {
default:
// Flag the symlink target as processed.
done[resolved] = Found
}
@@ -36,7 +38,7 @@ func SkipWalk(name string, isDir, isSymlink bool, done Done, ignore *IgnoreList)
skip = true
result = filepath.SkipDir
}
} else if isDir {
case isDir:
skip = true
if _ = ignore.Path(name); ignore.Ignore(name) || isDone {
@@ -46,9 +48,11 @@ func SkipWalk(name string, isDir, isSymlink bool, done Done, ignore *IgnoreList)
// Skip directories that contain a .ppstorage file.
result = filepath.SkipDir
}
} else if ignore.Ignore(name) || isDone {
// Skip files that are hidden or already done.
skip = true
default:
if ignore.Ignore(name) || isDone {
// Skip files that are hidden or already done.
skip = true
}
}
if skip {

View File

@@ -29,7 +29,7 @@ func WriteFile(fileName string, data []byte, perm os.FileMode) error {
}
}
file, err := os.OpenFile(fileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, perm)
file, err := os.OpenFile(fileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, perm) //nolint:gosec // caller-controlled path; intended write
if err != nil {
return err
@@ -68,7 +68,7 @@ func WriteFileFromReader(fileName string, reader io.Reader) (err error) {
var file *os.File
if file, err = os.OpenFile(fileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, ModeFile); err != nil {
if file, err = os.OpenFile(fileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, ModeFile); err != nil { //nolint:gosec // caller-controlled path; intended write
return err
}

View File

@@ -129,7 +129,7 @@ func TestWriteFileFromReader(t *testing.T) {
assert.NoError(t, writeErr)
assert.True(t, unixTime >= time.Now().Unix())
fileReader, readerErr := os.Open(filePath1)
fileReader, readerErr := os.Open(filePath1) //nolint:gosec // test helper reads temp file
assert.NoError(t, readerErr)
fileErr := WriteFileFromReader(filePath2, fileReader)
@@ -172,7 +172,7 @@ func TestCacheFileFromReader(t *testing.T) {
assert.NoError(t, writeErr)
assert.True(t, unixTime >= time.Now().Unix())
fileReader, readerErr := os.Open(filePath1)
fileReader, readerErr := os.Open(filePath1) //nolint:gosec // test helper reads temp file
assert.NoError(t, readerErr)
cacheFile, cacheErr := CacheFileFromReader(filePath2, fileReader)
@@ -208,7 +208,7 @@ func TestWriteFile_Truncates(t *testing.T) {
p := filepath.Join(dir, "f.txt")
assert.NoError(t, os.WriteFile(p, []byte("LONGDATA"), ModeFile))
assert.NoError(t, WriteFile(p, []byte("short"), ModeFile))
b, err := os.ReadFile(p)
b, err := os.ReadFile(p) //nolint:gosec // test helper reads temp file
assert.NoError(t, err)
assert.Equal(t, "short", string(b))
}

View File

@@ -24,7 +24,7 @@ func Zip(zipName string, files []string, compress bool) (err error) {
var newZipFile *os.File
if newZipFile, err = os.Create(zipName); err != nil {
if newZipFile, err = os.Create(zipName); err != nil { //nolint:gosec // zipName provided by caller
return err
}
@@ -46,7 +46,7 @@ func Zip(zipName string, files []string, compress bool) (err error) {
// ZipFile adds a file to a zip archive, optionally with an alias and compression.
func ZipFile(zipWriter *zip.Writer, fileName, fileAlias string, compress bool) (err error) {
// Open file.
fileToZip, err := os.Open(fileName)
fileToZip, err := os.Open(fileName) //nolint:gosec // fileName provided by caller
if err != nil {
return err
@@ -174,7 +174,7 @@ func unzipFileWithLimit(f *zip.File, dir string, fileSizeLimit int64) (fileName
return fileName, err
}
fd, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
fd, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) //nolint:gosec // destination derived from safeJoin
if err != nil {
return fileName, err
}

View File

@@ -14,7 +14,7 @@ import (
func writeZip(t *testing.T, path string, entries map[string][]byte) {
t.Helper()
f, err := os.Create(path)
f, err := os.Create(path) //nolint:gosec // test helper creates temp zip file
if err != nil {
t.Fatal(err)
}
@@ -62,7 +62,7 @@ func TestUnzip_SkipRulesAndLimits(t *testing.T) {
// ok2 (1 byte) allowed; total limit reduces to 2; nothing else left that fits
assert.ElementsMatch(t, []string{filepath.Join(outDir, "ok2.txt")}, files)
// Ensure file written
b, rerr := os.ReadFile(filepath.Join(outDir, "ok2.txt"))
b, rerr := os.ReadFile(filepath.Join(outDir, "ok2.txt")) //nolint:gosec // test helper reads temp file
assert.NoError(t, rerr)
assert.Equal(t, []byte("x"), b)
// Skipped contains at least the three excluded entries
@@ -213,7 +213,7 @@ func writeZip64Stub(t *testing.T, path, name string, size uint64) {
if len(filename) > math.MaxUint16 {
t.Fatalf("filename too long")
}
writeLE(uint16(len(filename)))
writeLE(uint16(len(filename))) //nolint:gosec // filename length checked above
writeLE(localExtraLen)
bw(filename)
// zip64 extra
@@ -239,7 +239,7 @@ func writeZip64Stub(t *testing.T, path, name string, size uint64) {
if len(filename) > math.MaxUint16 {
t.Fatalf("filename too long")
}
writeLE(uint16(len(filename)))
writeLE(uint16(len(filename))) //nolint:gosec // filename length checked above
writeLE(centralExtraLen)
writeLE(uint16(0)) // comment len
writeLE(uint16(0)) // disk start
@@ -264,9 +264,9 @@ func writeZip64Stub(t *testing.T, path, name string, size uint64) {
if centralLen > math.MaxUint32 || localLen > math.MaxUint32 {
t.Fatalf("central or local length exceeds uint32")
}
writeLE(uint32(centralLen))
writeLE(uint32(localLen))
writeLE(uint16(0)) // comment length
writeLE(uint32(centralLen)) //nolint:gosec // lengths checked above
writeLE(uint32(localLen)) //nolint:gosec
writeLE(uint16(0)) // comment length
if err := os.WriteFile(path, buf, 0o600); err != nil {
t.Fatal(err)

View File

@@ -5,9 +5,12 @@ import (
)
const (
DistLimit float64 = 5000
// DistLimit is the maximum distance in km considered realistic.
DistLimit float64 = 5000
// ScopeDistLimit is the maximum distance in km for scope queries.
ScopeDistLimit float64 = 50
DefaultDist float64 = 2
// DefaultDist is the default distance in km used when none is provided.
DefaultDist float64 = 2
)
// Deg returns the distance in decimal degrees based on the specified distance in meters and the latitude,
@@ -22,11 +25,12 @@ func Deg(lat, meter float64) (dLat, dLng float64) {
// Do not calculate the exact longitude distance in
// degrees if the latitude is zero or out of range.
if lat == 0.0 {
switch {
case lat == 0.0:
return dLat, dLat
} else if lat < -89.9 {
case lat < -89.9:
lat = -89.9
} else if lat > 89.9 {
case lat > 89.9:
lat = 89.9
}

View File

@@ -25,8 +25,12 @@ Additional information can be found in our Developer Guide:
package geo
const (
AverageEarthRadiusKm = 6371.0 // Global-average earth radius in km
AverageEarthRadiusMeter = AverageEarthRadiusKm * 1000.0 // Global-average earth radius in m
WGS84EarthRadiusKm = 6378.137 // WGS84 earth radius in km
WGS84EarthRadiusMeter = WGS84EarthRadiusKm * 1000.0 // WGS84 earth radius in m
// AverageEarthRadiusKm is the global-average earth radius in km.
AverageEarthRadiusKm = 6371.0
// AverageEarthRadiusMeter is the global-average earth radius in meters.
AverageEarthRadiusMeter = AverageEarthRadiusKm * 1000.0
// WGS84EarthRadiusKm is the WGS84 equatorial earth radius in km.
WGS84EarthRadiusKm = 6378.137
// WGS84EarthRadiusMeter is the WGS84 equatorial earth radius in meters.
WGS84EarthRadiusMeter = WGS84EarthRadiusKm * 1000.0
)

View File

@@ -2,6 +2,7 @@ package latlng
import "math"
// RoundDecimals defines the precision used when rounding coordinates.
var RoundDecimals = float64(10000000)
// Round rounds the given coordinate to six decimal places.

View File

@@ -140,32 +140,34 @@ func (m *Movement) Realistic() bool {
// AverageAltitude returns the average altitude.
func (m *Movement) AverageAltitude() float64 {
if m.Start.Altitude != 0 && m.End.Altitude == 0 {
switch {
case m.Start.Altitude != 0 && m.End.Altitude == 0:
return m.Start.Altitude
} else if m.Start.Altitude == 0 && m.End.Altitude != 0 {
case m.Start.Altitude == 0 && m.End.Altitude != 0:
return m.End.Altitude
} else if m.Start.Altitude != 0 && m.End.Altitude != 0 {
case m.Start.Altitude != 0 && m.End.Altitude != 0:
return (m.Start.Altitude + m.End.Altitude) / 2
default:
return 0
}
return 0
}
// EstimateAccuracy returns the position estimate accuracy in meter.
func (m *Movement) EstimateAccuracy(t time.Time) int {
var a float64
if !m.Realistic() {
switch {
case !m.Realistic():
a = m.Meter() / 2
} else if t.Before(m.Start.Time) {
case t.Before(m.Start.Time):
d := m.Start.Time.Sub(t).Hours() * 1000
d = math.Copysign(math.Sqrt(math.Abs(d)), d)
a = m.Speed() * d
} else if t.After(m.End.Time) {
case t.After(m.End.Time):
d := t.Sub(m.End.Time).Hours() * 1000
d = math.Copysign(math.Sqrt(math.Abs(d)), d)
a = m.Speed() * d
} else {
default:
a = m.Meter() / 20
}

View File

@@ -6,6 +6,7 @@ import (
"time"
)
// Meter represents one meter in decimal degrees at the equator.
const Meter = 0.00001
// Position represents a geo coordinate.

View File

@@ -1,10 +1,19 @@
package geo
import (
"math/rand/v2"
"crypto/rand"
"math/big"
)
// Randomize adds a random offset to a value.
func Randomize(value, diameter float64) float64 {
return value + (rand.Float64()-0.5)*diameter
// Use crypto/rand to avoid predictable offsets.
// randomFloat in [0,1)
n, err := rand.Int(rand.Reader, big.NewInt(1_000_000_000))
if err != nil {
return value
}
randomFloat := float64(n.Int64()) / 1_000_000_000.0
return value + (randomFloat-0.5)*diameter
}

View File

@@ -4,6 +4,7 @@ import (
"strings"
)
// TokenPrefix is the optional prefix for S2 tokens.
var TokenPrefix = "s2:"
// NormalizeToken removes the prefix from a token and converts all characters to lower case.

View File

@@ -1,7 +1,7 @@
package header
import (
"crypto/sha1"
"crypto/sha1" //nolint:gosec // SHA1 retained for legacy cache key hashing
"encoding/base64"
"fmt"
"net/http"
@@ -13,7 +13,7 @@ import (
// Authentication header names.
const (
Auth = "Authorization" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization
XAuthToken = "X-Auth-Token"
XAuthToken = "X-Auth-Token" //nolint:gosec // header name, not a secret
XSessionID = "X-Session-ID"
)
@@ -98,7 +98,7 @@ func BasicAuth(c *gin.Context) (username, password, cacheKey string) {
return "", "", ""
}
cacheKey = fmt.Sprintf("%x", sha1.Sum([]byte(authToken)))
cacheKey = fmt.Sprintf("%x", sha1.Sum([]byte(authToken))) //nolint:gosec // cache key only
return credentials[0], credentials[1], cacheKey
}

View File

@@ -219,7 +219,7 @@ func TestAuthorization(t *testing.T) {
Header: make(http.Header),
}
token := "eyJhbGciOiJFZERTQSIsImtpZCI6IjEyMyJ9.eyJpc3MiOiJwb3J0YWw6dGVzdCIsImF1ZCI6Im5vZGU6YWJjIiwiZXhwIjoxNzAwMDAwMDB9.dGVzdC1zaWduYXR1cmUtYnl0ZXM"
token := "eyJhbGciOiJFZERTQSIsImtpZCI6IjEyMyJ9.eyJpc3MiOiJwb3J0YWw6dGVzdCIsImF1ZCI6Im5vZGU6YWJjIiwiZXhwIjoxNzAwMDAwMDB9.dGVzdC1zaWduYXR1cmUtYnl0ZXM" //nolint:gosec // static test token
c.Request.Header.Add(Auth, "Bearer "+token)
authType, authToken := Authorization(c)

View File

@@ -7,8 +7,7 @@ import (
)
const (
// The CacheControl request and response header field contains directives (instructions)
// that control caching in browsers and shared caches (e.g. proxies, CDNs).
// CacheControl request and response header field contains directives for caching.
// See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
CacheControl = "Cache-Control"

View File

@@ -16,6 +16,7 @@ const (
)
var (
// CdnMethods lists HTTP methods allowed via CDN.
CdnMethods = []string{http.MethodGet, http.MethodHead, http.MethodOptions}
)
@@ -36,13 +37,14 @@ func IsCdn(req *http.Request) bool {
// AbortCdnRequest checks if the request should not be served through a CDN.
func AbortCdnRequest(req *http.Request) bool {
if !IsCdn(req) {
switch {
case !IsCdn(req):
return false
} else if req.Header.Get(XAuthToken) != "" {
case req.Header.Get(XAuthToken) != "":
return true
} else if req.URL.Path == "/" {
case req.URL.Path == "/":
return true
default:
return list.Excludes(CdnMethods, req.Method)
}
return list.Excludes(CdnMethods, req.Method)
}

View File

@@ -1,7 +1,10 @@
package header
const (
CidrPodInternal = "10.0.0.0/8"
// CidrPodInternal covers internal pod traffic ranges.
CidrPodInternal = "10.0.0.0/8"
// CidrDockerInternal covers default Docker internal ranges.
CidrDockerInternal = "172.16.0.0/12"
// CidrCalicoInternal covers Calico internal ranges.
CidrCalicoInternal = "192.168.0.0/16"
)

View File

@@ -10,6 +10,7 @@ import (
var IpRegExp = regexp.MustCompile(`[^a-zA-Z0-9:.]`)
const (
// IPv6Length represents the maximum length of an IPv6 address string.
IPv6Length = 39
)
@@ -29,7 +30,8 @@ func IP(s, defaultIp string) string {
fastOK := true
for i := 0; i < len(s); i++ {
b := s[i]
if !((b >= '0' && b <= '9') || (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == ':' || b == '.') {
isAlphaNum := (b >= '0' && b <= '9') || (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')
if !isAlphaNum && b != ':' && b != '.' {
fastOK = false
break
}

View File

@@ -1,7 +1,10 @@
package header
var (
ProtoHttp = "http"
// ProtoHttp is the HTTP scheme.
ProtoHttp = "http"
// ProtoHttps is the HTTPS scheme.
ProtoHttps = "https"
ProtoWss = "wss"
// ProtoWss is the secure WebSocket scheme.
ProtoWss = "wss"
)

View File

@@ -1,5 +1,6 @@
package header
// RobotsRule represents a robots.txt directive rule.
type RobotsRule = string
// RobotsTag controls how pages are indexed and crawled by search engines:

View File

@@ -1,6 +1,8 @@
package header
const (
Any = "*"
// Any wildcard value for header lists.
Any = "*"
// Deny disallows embedding/access (used in frame/permission headers).
Deny = "DENY"
)

View File

@@ -1,6 +1,8 @@
package header
const (
// XFavorite marks favorite status in WebDAV headers.
XFavorite = "X-Favorite"
XModTime = "X-OC-MTime"
// XModTime conveys modification time in WebDAV headers.
XModTime = "X-OC-MTime"
)

View File

@@ -1,8 +1,12 @@
package header
const (
WebhookID string = "webhook-id"
WebhookSignature string = "webhook-signature"
WebhookTimestamp string = "webhook-timestamp"
// WebhookID is the request header containing a webhook identifier.
WebhookID string = "webhook-id"
// WebhookSignature carries the signature header.
WebhookSignature string = "webhook-signature"
// WebhookTimestamp carries the timestamp header.
WebhookTimestamp string = "webhook-timestamp"
// WebhookSecretPrefix prefixes stored webhook secrets.
WebhookSecretPrefix string = "whsec_"
)

View File

@@ -153,12 +153,12 @@ func Download(destPath, rawURL string, opt *Options) error {
}
tmp := destPath + ".part"
f, err := os.OpenFile(tmp, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
f, err := os.OpenFile(tmp, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600) //nolint:gosec // destPath validated by caller; temp file
if err != nil {
return err
}
defer func() {
f.Close()
_ = f.Close()
if err != nil {
_ = os.Remove(tmp)
}
@@ -180,10 +180,8 @@ func Download(destPath, rawURL string, opt *Options) error {
if err = f.Close(); err != nil {
return err
}
if err = os.Rename(tmp, destPath); err != nil {
return err
}
return nil
return os.Rename(tmp, destPath)
}
func isPrivateOrDisallowedIP(ip net.IP) bool {

View File

@@ -50,7 +50,7 @@ func TestDownload_AllowRedirectToPrivate(t *testing.T) {
if err := Download(dest, ts.URL, &Options{Timeout: 5 * time.Second, MaxSizeBytes: 1 << 20, AllowPrivate: true}); err != nil {
t.Fatalf("unexpected error: %v", err)
}
b, err := os.ReadFile(dest)
b, err := os.ReadFile(dest) //nolint:gosec // test reads temp file
if err != nil || string(b) != "ok" {
t.Fatalf("unexpected content: %v %q", err, string(b))
}

View File

@@ -21,7 +21,7 @@ func TestSafeDownload_OK(t *testing.T) {
if err := Download(dest, ts.URL, &Options{Timeout: 5 * time.Second, MaxSizeBytes: 1024, AllowPrivate: true}); err != nil {
t.Fatal(err)
}
b, err := os.ReadFile(dest)
b, err := os.ReadFile(dest) //nolint:gosec // test reads temp file
if err != nil || string(b) != "hello" {
t.Fatalf("unexpected content: %v %q", err, string(b))
}

View File

@@ -21,9 +21,12 @@ var (
defaultTimeout = 30 * time.Second
defaultMaxSize = int64(200 * 1024 * 1024) // 200 MiB
// ErrSchemeNotAllowed is returned when a URL scheme is not permitted.
ErrSchemeNotAllowed = errors.New("invalid scheme (only http/https allowed)")
ErrSizeExceeded = errors.New("response exceeds maximum allowed size")
ErrPrivateIP = errors.New("connection to private or loopback address not allowed")
// ErrSizeExceeded is returned when a response exceeds the configured limit.
ErrSizeExceeded = errors.New("response exceeds maximum allowed size")
// ErrPrivateIP is returned when the target resolves to a private or loopback address.
ErrPrivateIP = errors.New("connection to private or loopback address not allowed")
)
// envInt64 returns an int64 from env or -1 if unset/invalid.

View File

@@ -4,19 +4,31 @@ package scheme
type Type = string
const (
File Type = "file"
Data Type = "data"
Base64 Type = "base64"
Http Type = "http"
Https Type = "https"
Websocket Type = "wss"
Unix Type = "unix"
HttpUnix Type = "http+unix"
Unixgram Type = "unixgram"
// File scheme.
File Type = "file"
// Data scheme.
Data Type = "data"
// Base64 scheme.
Base64 Type = "base64"
// Http scheme.
Http Type = "http"
// Https scheme.
Https Type = "https"
// Websocket scheme (secure).
Websocket Type = "wss"
// Unix scheme.
Unix Type = "unix"
// HttpUnix scheme.
HttpUnix Type = "http+unix"
// Unixgram scheme.
Unixgram Type = "unixgram"
// Unixpacket scheme.
Unixpacket Type = "unixpacket"
)
var (
// HttpsData lists allowed schemes (https, data).
HttpsData = []string{Https, Data}
// HttpsHttp lists allowed schemes (https, http).
HttpsHttp = []string{Https, Http}
)

View File

@@ -34,7 +34,10 @@ import (
//go:generate xgettext --no-wrap --language=c --from-code=UTF-8 --output=../../assets/locales/messages.pot messages.go
// Message represents a localized message identifier.
type Message int
// MessageMap maps message IDs to their localized strings.
type MessageMap map[Message]string
var noVars []interface{}

View File

@@ -6,30 +6,45 @@ import (
"github.com/leonelquinteros/gotext"
)
// Locale represents a language/region tag (e.g., "en", "pt_BR").
type Locale string
const (
German Locale = "de"
English Locale = "en"
Spanish Locale = "es"
French Locale = "fr"
Dutch Locale = "nl"
Polish Locale = "pl"
Portuguese Locale = "pt"
// German locale.
German Locale = "de"
// English locale.
English Locale = "en"
// Spanish locale.
Spanish Locale = "es"
// French locale.
French Locale = "fr"
// Dutch locale.
Dutch Locale = "nl"
// Polish locale.
Polish Locale = "pl"
// Portuguese locale.
Portuguese Locale = "pt"
// BrazilianPortuguese locale.
BrazilianPortuguese Locale = "pt_BR"
Russian Locale = "ru"
ChineseSimplified Locale = "zh"
ChineseTraditional Locale = "zh_TW"
Default = English
// Russian locale.
Russian Locale = "ru"
// ChineseSimplified locale.
ChineseSimplified Locale = "zh"
// ChineseTraditional locale.
ChineseTraditional Locale = "zh_TW"
// Default locale used when none is supplied.
Default = English
)
var localeDir = "../../assets/locales"
var locale = Default
// SetDir sets the path to the locales directory.
func SetDir(dir string) {
localeDir = dir
}
// SetLocale sets the current locale.
func SetLocale(loc string) {
switch len(loc) {
case 2:
@@ -45,6 +60,7 @@ func SetLocale(loc string) {
gotext.Configure(localeDir, string(locale), "default")
}
// Locale returns the string value of the locale.
func (l Locale) Locale() string {
return string(l)
}

View File

@@ -1,7 +1,10 @@
package i18n
// Message and Error identifiers.
const (
// ErrUnexpected is returned for unexpected errors.
ErrUnexpected Message = iota + 1
// ErrBadRequest indicates malformed input.
ErrBadRequest
ErrSaveFailed
ErrDeleteFailed
@@ -99,6 +102,7 @@ const (
MsgActivated
)
// Messages holds default English message strings.
var Messages = MessageMap{
// Error messages:
ErrUnexpected: gettext("Something went wrong, try again"),

View File

@@ -2,6 +2,7 @@ package i18n
import "strings"
// Response represents an i18n-aware response payload.
type Response struct {
Code int `json:"code"`
Err string `json:"error,omitempty"`
@@ -17,6 +18,7 @@ func (r Response) String() string {
}
}
// LowerString returns the lowercased message string.
func (r Response) LowerString() string {
return strings.ToLower(r.String())
}
@@ -25,10 +27,12 @@ func (r Response) Error() string {
return r.Err
}
// Success reports whether the response code indicates success (2xx).
func (r Response) Success() bool {
return r.Err == "" && r.Code < 400
}
// NewResponse builds a Response with the given code, message ID, and optional parameters.
func NewResponse(code int, id Message, params ...interface{}) Response {
if code < 400 {
return Response{Code: code, Msg: Msg(id, params...)}

View File

@@ -2,13 +2,14 @@ package list
// Add adds a string to the list if it does not exist yet.
func Add(list []string, s string) []string {
if s == "" {
switch {
case s == "":
return list
} else if len(list) == 0 {
case len(list) == 0:
return []string{s}
} else if Contains(list, s) {
case Contains(list, s):
return list
default:
return append(list, s)
}
return append(list, s)
}

View File

@@ -45,9 +45,8 @@ func (list Attr) Strings() []string {
if s == "" {
continue
} else if i == 0 {
// Skip check.
} else if result[i-1] == s {
}
if i > 0 && result[i-1] == s {
continue
}
@@ -68,13 +67,14 @@ func (list Attr) Strings() []string {
// Sort sorts the attributes by key.
func (list Attr) Sort() Attr {
sort.Slice(list, func(i, j int) bool {
if list[i].Key == list[j].Key {
switch {
case list[i].Key == list[j].Key:
return list[i].Value < list[j].Value
} else if list[i].Key == Any {
case list[i].Key == Any:
return false
} else if list[j].Key == Any {
case list[j].Key == Any:
return true
} else {
default:
return list[i].Key < list[j].Key
}
})
@@ -122,11 +122,12 @@ func (list Attr) Find(s string) (a KeyValue) {
} else {
for i := range list {
if strings.EqualFold(attr.Key, list[i].Key) {
if attr.Value == enum.True && list[i].Value == enum.False {
switch {
case attr.Value == enum.True && list[i].Value == enum.False:
return KeyValue{Key: "", Value: ""}
} else if attr.Value == list[i].Value {
case attr.Value == list[i].Value:
return *list[i]
} else if list[i].Value == Any {
case list[i].Value == Any:
a = *list[i]
}
} else if list[i].Key == Any && attr.Value != enum.False {

View File

@@ -30,12 +30,8 @@ func ContainsAny(l, s []string) bool {
return false
}
// If second list contains All, it's a wildcard match.
if s[0] == Any {
return true
}
for j := 1; j < len(s); j++ {
if s[j] == Any {
for _, v := range s {
if v == Any {
return true
}
}

View File

@@ -51,7 +51,7 @@ func (l *list[K, V]) Back() *Element[K, V] {
return l.root.prev
}
// Remove detaches e from the list while keeping the remaining neighbours
// Remove detaches e from the list while keeping the remaining neighbors
// correctly linked. After removal the element's next/prev references are
// zeroed so the node can be safely re-used or left for GC without retaining
// other elements.

View File

@@ -130,7 +130,7 @@ func (m *Map[K, V]) AllFromBack() iter.Seq2[K, V] {
}
// Keys returns an iterator that yields all keys in insertion order. Use
// slices.Collect(m.Keys()) if a materialised slice is required.
// slices.Collect(m.Keys()) if a materialized slice is required.
func (m *Map[K, V]) Keys() iter.Seq[K] {
return func(yield func(key K) bool) {
for el := m.Front(); el != nil; el = el.Next() {

View File

@@ -1,5 +1,7 @@
package ordered_test
//revive:disable:var-naming // benchmark helpers follow Go benchmark naming with underscores
import (
"slices"
"strconv"
@@ -493,7 +495,7 @@ func benchmarkOrderedMap_Len(multiplier int) func(b *testing.B) {
temp = m.Len()
}
// prevent compiler from optimising Len away.
// prevent compiler from optimizing Len away.
tempInt = temp
}
}
@@ -800,7 +802,7 @@ func BenchmarkOrderedMapString_Has(b *testing.B) {
}
func nothing(v interface{}) {
v = false
_ = v
}
func benchmarkBigMap_Set() func(b *testing.B) {

View File

@@ -2,7 +2,7 @@ package ordered
import (
"fmt"
"math/rand"
"math/rand" //nolint:gosec // pseudo-random is sufficient for concurrency tests
"sync"
"testing"
)
@@ -10,11 +10,15 @@ import (
func TestRaceCondition(t *testing.T) {
m := NewSyncMap[int, int]()
wg := &sync.WaitGroup{}
//nolint:gosec // pseudo-random is sufficient for race testing
randInt := func() int {
return rand.Intn(100)
}
var asyncGet = func() {
wg.Add(1)
go func() {
key := rand.Intn(100)
key := randInt()
m.Get(key)
wg.Done()
}()
@@ -23,8 +27,8 @@ func TestRaceCondition(t *testing.T) {
var asyncSet = func() {
wg.Add(1)
go func() {
key := rand.Intn(100)
value := rand.Intn(100)
key := randInt()
value := randInt()
m.Set(key, value)
wg.Done()
}()
@@ -33,7 +37,7 @@ func TestRaceCondition(t *testing.T) {
var asyncDelete = func() {
wg.Add(1)
go func() {
key := rand.Intn(100)
key := randInt()
m.Delete(key)
wg.Done()
}()
@@ -42,7 +46,7 @@ func TestRaceCondition(t *testing.T) {
var asyncHas = func() {
wg.Add(1)
go func() {
key := rand.Intn(100)
key := randInt()
m.Has(key)
wg.Done()
}()
@@ -51,8 +55,8 @@ func TestRaceCondition(t *testing.T) {
var asyncReplaceKEy = func() {
wg.Add(1)
go func() {
key := rand.Intn(100)
newKey := rand.Intn(100)
key := randInt()
newKey := randInt()
m.ReplaceKey(key, newKey)
wg.Done()
}()
@@ -61,8 +65,8 @@ func TestRaceCondition(t *testing.T) {
var asyncGetOrDefault = func() {
wg.Add(1)
go func() {
key := rand.Intn(100)
def := rand.Intn(100)
key := randInt()
def := randInt()
m.GetOrDefault(key, def)
wg.Done()
}()

View File

@@ -1,5 +1,7 @@
package dummy
//revive:disable:exported
import (
"context"
"io"

View File

@@ -20,7 +20,7 @@ func TestError(t *testing.T) {
},
{
name: "SanitizeSpecialCharacters",
err: errors.New("permission denied { DROP TABLE users; }\n"),
err: errors.New("permission denied { DROP TABLE users; }"),
},
{
name: "WhitespaceOnly",

View File

@@ -1,19 +1,20 @@
Hybrid Photo/Video File Support
===============================
## PhotoPrism — Media Package
## Apple iPhone and iPad
**Last Updated:** November 22, 2025
### Apple iPhone and iPad
[iOS Live Photos](https://developer.apple.com/live-photos/) consist of a JPEG/HEIC image and a QuickTime AVC/HEVC video, which are both required for viewing.
We recommend [using an app like PhotoSync](https://docs.photoprism.app/user-guide/sync/mobile-devices/#photosync) to upload Live Photos to PhotoPrism, since the iOS web upload usually only submits the HEIC image file without the video.
## Android Devices
### Android Devices
Some Samsung and Google Android devices support taking "Motion Photos" with the included Camera app. Motion Photos are JPEG/HEIC image with a short MP4 video embedded after the image data.
The image part of these files can be opened in any image viewer that supports JPEG/HEIC, but the video part cannot. However, since the MP4 video is simply appended at the end of the image file, it can be easily read by our software and streamed through the API as needed.
## Introductory Tutorials
### Introductory Tutorials
| Title | Date | URL |
|---------------------------------------------------------|----------|------------------------------------------------------------------------------------|
@@ -24,7 +25,7 @@ The image part of these files can be opened in any image viewer that supports JP
| Working with Motion Photos | Jan 2019 | https://medium.com/android-news/working-with-motion-photos-da0aa49b50c |
| Google: Behind the Motion Photos Technology in Pixel 2 | Mar 2018 | https://blog.research.google/2018/03/behind-motion-photos-technology-in.html |
## Software Libraries and References
### Software Libraries and References
| Title | URL |
|------------------------------------------------------|-------------------------------------------------------------------------|
@@ -37,19 +38,15 @@ The image part of these files can be opened in any image viewer that supports JP
| How to use the io.Reader interface | https://yourbasic.org/golang/io-reader-interface-explained/ |
| AV1 Codec ISO Media File Format | https://aomediacodec.github.io/av1-isobmff |
## Related GitHub Issues
### Related GitHub Issues
- https://github.com/photoprism/photoprism/issues/439 (Samsung: Initial support for Motion Photos)
- https://github.com/photoprism/photoprism/issues/1739 (Google: Initial support for Motion Photos)
- https://github.com/photoprism/photoprism/issues/2788 (Metadata: Flag Samsung/Google Motion Photos as Live Photos)
- https://github.com/cliveontoast/GoMoPho/issues/23 (Google Motion Photos Video Extractor: Add Android 12 Support)
## Related Pull Requests
### Related Pull Requests
- https://github.com/photoprism/photoprism/pull/3709 (Google: Initial support for Motion Photos)
- https://github.com/photoprism/photoprism/pull/3722 (Google: Add support for Motion Photos)
- https://github.com/photoprism/photoprism/pull/3660 (Samsung: Improved support for Motion Photos)
----
*PhotoPrism® is a [registered trademark](https://www.photoprism.app/trademark). By using the software and services we provide, you agree to our [Terms of Service](https://www.photoprism.app/terms), [Privacy Policy](https://www.photoprism.app/privacy), and [Code of Conduct](https://www.photoprism.app/code-of-conduct). Docs are [available](https://link.photoprism.app/github-docs) under the [CC BY-NC-SA 4.0 License](https://creativecommons.org/licenses/by-nc-sa/4.0/); [additional terms](https://github.com/photoprism/photoprism/blob/develop/assets/README.md) may apply.*

View File

@@ -23,7 +23,7 @@ func (c Chroma) Hex() string {
// Uint returns the colourfulness in percent as unsigned integer.
func (c Chroma) Uint() uint {
return uint(c.Percent())
return uint(c.Percent()) //nolint:gosec // Percent is bounded 0..100
}
// Int returns the colourfulness in percent as integer.

View File

@@ -30,28 +30,48 @@ import (
"github.com/photoprism/photoprism/pkg/txt"
)
// Color represents a indexed color value.
type Color int16
// Colors is a slice of Color values.
type Colors []Color
const (
// Black color.
Black Color = iota
// Grey color.
Grey
// Brown color.
Brown
// Gold color.
Gold
// White color.
White
// Purple color.
Purple
// Blue color.
Blue
// Cyan color.
Cyan
// Teal color.
Teal
// Green color.
Green
// Lime color.
Lime
// Yellow color.
Yellow
// Magenta color.
Magenta
// Orange color.
Orange
// Red color.
Red
// Pink color.
Pink
)
// All lists all defined colors in display order.
var All = Colors{
Purple,
Magenta,
@@ -71,6 +91,7 @@ var All = Colors{
Black,
}
// Names maps Color to their lowercase names.
var Names = map[Color]string{
Black: "black", // 0
Grey: "grey", // 1
@@ -90,6 +111,7 @@ var Names = map[Color]string{
Pink: "pink", // F
}
// Weights assigns relative importance to colors.
var Weights = map[Color]uint16{
Grey: 1,
Black: 2,
@@ -109,14 +131,17 @@ var Weights = map[Color]uint16{
Magenta: 5,
}
// Name returns the lowercase name for the color.
func (c Color) Name() string {
return Names[c]
}
// ID returns the numeric identifier for the color.
func (c Color) ID() int16 {
return int16(c)
}
// Hex returns the hex nibble for the color or "0" if out of range.
func (c Color) Hex() string {
if c < 0 || c > 15 {
return "0"
@@ -125,6 +150,7 @@ func (c Color) Hex() string {
return fmt.Sprintf("%X", c)
}
// Hex returns the concatenated hex values for the slice.
func (c Colors) Hex() (result string) {
for _, indexedColor := range c {
result += indexedColor.Hex()
@@ -133,6 +159,7 @@ func (c Colors) Hex() (result string) {
return result
}
// List returns a slice of maps with slug, display name, and example color.
func (c Colors) List() []map[string]string {
result := make([]map[string]string, 0, len(c))

View File

@@ -1,5 +1,6 @@
package colors
// ColorExamples contains representative hex values for each Color.
var ColorExamples = map[Color]string{
Black: "#212121",
Grey: "#9E9E9E",

View File

@@ -1,5 +1,6 @@
package colors
// LightMap stores luminance values for a palette.
type LightMap []Luminance
// Hex returns all luminance value as a hex encoded string.
@@ -69,7 +70,7 @@ func (m LightMap) Diff() (result int) {
result = 1
for _, val := range diffValues {
result = result << 1
result <<= 1
a := 0
b := 0
@@ -83,7 +84,7 @@ func (m LightMap) Diff() (result int) {
}
if a+4 > b {
result += 1
result++
}
}

View File

@@ -63,16 +63,16 @@ func TestLightMap_Diff(t *testing.T) {
t.Run("Happy", func(t *testing.T) {
m1 := LightMap{8, 13, 7, 2, 2, 3, 6, 3, 4}
d1 := m1.Diff()
t.Log(strconv.FormatUint(uint64(d1), 2))
t.Log(strconv.FormatUint(uint64(uint16(d1)), 2)) //nolint:gosec // test logging
m2 := LightMap{8, 13, 7, 3, 1, 3, 5, 3, 4}
d2 := m2.Diff()
t.Log(strconv.FormatUint(uint64(d2), 2))
t.Log(strconv.FormatUint(uint64(uint16(d2)), 2)) //nolint:gosec // test logging
m3 := LightMap{9, 13, 7, 8, 2, 4, 5, 3, 4}
d3 := m3.Diff()
t.Log(strconv.FormatUint(uint64(d3), 2))
t.Log(strconv.FormatUint(uint64(uint16(d3)), 2)) //nolint:gosec // test logging
m4 := LightMap{9, 13, 7, 7, 2, 4, 6, 2, 3}
d4 := m4.Diff()
t.Log(strconv.FormatUint(uint64(d4), 2))
t.Log(strconv.FormatUint(uint64(uint16(d4)), 2)) //nolint:gosec // test logging
t.Logf("values: %d, %d, %d, %d", d1, d2, d3, d4)
})

View File

@@ -2,8 +2,10 @@ package colors
import "fmt"
// Luminance represents a luminance value.
type Luminance int16
// Hex returns the hex string for the luminance value.
func (l Luminance) Hex() string {
return fmt.Sprintf("%X", l)
}

View File

@@ -2,6 +2,7 @@ package colors
import "image/color"
// ColorMap maps RGBA values to Color enums.
var ColorMap = map[color.RGBA]Color{
{0x00, 0x00, 0x00, 0xff}: Black,
{0xe0, 0xe0, 0xe0, 0xff}: Grey,

View File

@@ -2,6 +2,7 @@ package colors
import "strings"
// Profile represents a color profile name.
type Profile string
// Supported color profiles.

View File

@@ -2,7 +2,8 @@ package colors
import (
"image"
_ "image/jpeg"
_ "image/jpeg" // register JPEG
_ "image/png" // register PNG (may appear in decoded sources)
"runtime"
"github.com/mandykoh/prism"

View File

@@ -11,7 +11,7 @@ import (
)
func writeImage(path string, img image.Image) error {
imgFile, err := os.Create(path)
imgFile, err := os.Create(path) //nolint:gosec // test temp file
if err != nil {
return err
@@ -32,7 +32,7 @@ func TestToSRGB(t *testing.T) {
t.Logf("testfile: %s", testFile)
imgFile, err := os.Open(testFile)
imgFile, err := os.Open(testFile) //nolint:gosec // test temp file
if err != nil {
t.Fatal(err)

View File

@@ -82,7 +82,7 @@ func ReadUrl(fileUrl string, schemes []string) (data []byte, err error) {
// Fetch the file data from the specified URL, depending on its scheme.
switch u.Scheme {
case scheme.Https, scheme.Http, scheme.Unix, scheme.HttpUnix:
resp, httpErr := http.Get(fileUrl)
resp, httpErr := http.Get(fileUrl) //nolint:gosec // URL already validated by caller; https/http only
if httpErr != nil {
return data, fmt.Errorf("invalid %s url (%s)", u.Scheme, httpErr)
@@ -100,7 +100,7 @@ func ReadUrl(fileUrl string, schemes []string) (data []byte, err error) {
return DecodeBase64String(binaryData)
}
case scheme.File:
if data, err = os.ReadFile(fileUrl); err != nil {
if data, err = os.ReadFile(fileUrl); err != nil { //nolint:gosec // fileUrl validated earlier
return data, fmt.Errorf("invalid %s url (%s)", u.Scheme, err)
}
default:

View File

@@ -117,7 +117,8 @@ func TestDataUrl_WebpDetection(t *testing.T) {
// Minimal RIFF/WEBP container header
// RIFF <size=26> WEBP VP8 + padding
riff := []byte{'R', 'I', 'F', 'F', 26, 0, 0, 0, 'W', 'E', 'B', 'P', 'V', 'P', '8', ' '}
buf := append(riff, bytes.Repeat([]byte{0}, 32)...)
riff = append(riff, bytes.Repeat([]byte{0}, 32)...)
buf := riff
s := DataUrl(bytes.NewReader(buf))
assert.True(t, strings.HasPrefix(s, "data:image/webp;base64,"))
}

View File

@@ -7,7 +7,9 @@ import "strings"
type Orientation = string
const (
KeepOrientation Orientation = "keep"
// KeepOrientation preserves existing orientation metadata.
KeepOrientation Orientation = "keep"
// ResetOrientation strips orientation metadata.
ResetOrientation Orientation = "reset"
)

View File

@@ -8,5 +8,8 @@ import (
"github.com/photoprism/photoprism/pkg/fs"
)
// PreviewFileTypes lists MIME types eligible for preview generation.
var PreviewFileTypes = []string{fs.ImageJpeg.String(), fs.ImagePng.String()}
// PreviewExpr is a SQL expression containing allowed preview MIME types.
var PreviewExpr = gorm.Expr("'" + strings.Join(PreviewFileTypes, "','") + "'")

View File

@@ -1,13 +1,20 @@
package projection
const (
Unknown Type = ""
Equirectangular Type = "equirectangular"
Cubestrip Type = "cubestrip"
Cylindrical Type = "cylindrical"
TransverseCylindrical Type = "transverse-cylindrical"
// Unknown projection.
Unknown Type = ""
// Equirectangular projection type.
Equirectangular Type = "equirectangular"
// Cubestrip projection type.
Cubestrip Type = "cubestrip"
// Cylindrical projection type.
Cylindrical Type = "cylindrical"
// TransverseCylindrical projection type.
TransverseCylindrical Type = "transverse-cylindrical"
// PseudocylindricalCompromise projection type.
PseudocylindricalCompromise Type = "pseudocylindrical-compromise"
Other Type = "other"
// Other projection type.
Other Type = "other"
)
// Types maps identifiers to known types.

View File

@@ -1,9 +1,12 @@
package media
// Src identifies a media source.
type Src = string
// Data source types.
const (
SrcLocal Src = "local"
// SrcLocal indicates the media originates from local storage.
SrcLocal Src = "local"
// SrcRemote indicates the media originates from a remote source.
SrcRemote Src = "remote"
)

View File

@@ -91,7 +91,7 @@ func FileTypeOffset(fileName string, brands Chunks) (int, error) {
return -1, errors.New("file not found")
}
file, err := os.Open(fileName)
file, err := os.Open(fileName) //nolint:gosec // fileName validated by caller
if err != nil {
return -1, err

View File

@@ -52,7 +52,7 @@ func (c Chunk) FileOffset(fileName string) (int, error) {
return -1, errors.New("file not found")
}
file, err := os.Open(fileName)
file, err := os.Open(fileName) //nolint:gosec // fileName validated by caller
if err != nil {
return -1, err

View File

@@ -5,9 +5,12 @@ package video
type Profile = string
const (
// ProfileBaseline indicates H.264 Baseline profile.
ProfileBaseline Profile = "Baseline"
ProfileMain Profile = "Main"
ProfileHigh Profile = "High"
// ProfileMain indicates H.264 Main profile.
ProfileMain Profile = "Main"
// ProfileHigh indicates H.264 High profile.
ProfileHigh Profile = "High"
)
// CodecProfile represents a codec subtype with its standardized ID,

Some files were not shown because too many files have changed in this diff Show More