精简后的项目:仅保留 api/ui 源码、文档、构建依赖

This commit is contained in:
gallery
2026-09-11 16:11:09 +08:00
commit 490e85634e
457 changed files with 86862 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
package utils
import (
"log"
"net"
"net/url"
"strconv"
"strings"
)
const defaultIP = "127.0.0.1"
const defaultPort = "4001"
const defaultAPIPrefix = "/api"
func ApiListenUrl() *url.URL {
apiPath := ApiEndpointUrl().Path
var listenAddr string
listenAddr = EnvListenIP.GetValue()
if listenAddr == "" {
listenAddr = defaultIP
}
// Validate listenAddr is a valid IP (IPv4 or IPv6)
if parsedIP := net.ParseIP(listenAddr); parsedIP == nil {
log.Panicf("%q must be a valid IPv4 or IPv6 address: %q", EnvListenIP.GetName(), listenAddr)
}
listenPortStr := EnvListenPort.GetValue()
if listenPortStr == "" {
listenPortStr = defaultPort
}
listenPort, err := strconv.Atoi(listenPortStr)
if err != nil {
log.Panicf("%q must be a number %q: %v", EnvListenPort.GetName(), listenPortStr, err)
}
if listenPort < 1 || listenPort > 65535 {
log.Panicf("%q must be a valid port number (1-65535): %d", EnvListenPort.GetName(), listenPort)
}
return &url.URL{
Scheme: "http",
Host: net.JoinHostPort(listenAddr, listenPortStr),
Path: apiPath,
}
}
func ApiEndpointUrl() *url.URL {
apiEndpointStr := EnvAPIEndpoint.GetValue()
if apiEndpointStr == "" {
apiEndpointStr = defaultAPIPrefix
}
apiEndpointURL, err := url.Parse(apiEndpointStr)
if err != nil {
log.Panicf("ERROR: Environment variable %s is not a proper url (%s): %v",
EnvAPIEndpoint.GetName(), EnvAPIEndpoint.GetValue(), err)
}
// If absolute URL with empty path (e.g. "https://host"), default to /api for backward compatibility.
if apiEndpointURL.Scheme != "" && apiEndpointURL.Host != "" && apiEndpointURL.Path == "" {
apiEndpointURL.Path = defaultAPIPrefix
}
// Ensure relative paths start with a leading slash.
if apiEndpointURL.Scheme == "" &&
apiEndpointURL.Host == "" &&
apiEndpointURL.Path != "" &&
!strings.HasPrefix(apiEndpointURL.Path, "/") {
apiEndpointURL.Path = "/" + apiEndpointURL.Path
}
return apiEndpointURL
}
func UiEndpointUrl() *url.URL {
shouldServeUI := ShouldServeUI()
if shouldServeUI {
return nil
}
uiEndpointURL, err := url.Parse(EnvUIEndpoint.GetValue())
if err != nil {
log.Panicf("ERROR: Environment variable %s is not a proper url (%s): %v",
EnvUIEndpoint.GetName(), EnvUIEndpoint.GetValue(), err)
}
return uiEndpointURL
}
+104
View File
@@ -0,0 +1,104 @@
package utils
import (
"os"
"strconv"
"strings"
"time"
"github.com/photoview/photoview/api/log"
)
// EnvironmentVariable represents the name of an environment variable used to configure Photoview
type EnvironmentVariable string
// General options
const (
EnvDevelopmentMode EnvironmentVariable = "PHOTOVIEW_DEVELOPMENT_MODE"
EnvServeUI EnvironmentVariable = "PHOTOVIEW_SERVE_UI"
EnvUIPath EnvironmentVariable = "PHOTOVIEW_UI_PATH"
EnvMediaCachePath EnvironmentVariable = "PHOTOVIEW_MEDIA_CACHE"
EnvFaceRecognitionModelsPath EnvironmentVariable = "PHOTOVIEW_FACE_RECOGNITION_MODELS_PATH"
EnvMediaProbeTimeout EnvironmentVariable = "PHOTOVIEW_MEDIA_PROBE_TIMEOUT"
)
// Network related
const (
EnvListenIP EnvironmentVariable = "PHOTOVIEW_LISTEN_IP"
EnvListenPort EnvironmentVariable = "PHOTOVIEW_LISTEN_PORT"
EnvAPIEndpoint EnvironmentVariable = "PHOTOVIEW_API_ENDPOINT"
EnvUIEndpoint EnvironmentVariable = "PHOTOVIEW_UI_ENDPOINT"
)
// Database related
const (
EnvDatabaseDriver EnvironmentVariable = "PHOTOVIEW_DATABASE_DRIVER"
EnvMysqlURL EnvironmentVariable = "PHOTOVIEW_MYSQL_URL"
EnvPostgresURL EnvironmentVariable = "PHOTOVIEW_POSTGRES_URL"
EnvSqlitePath EnvironmentVariable = "PHOTOVIEW_SQLITE_PATH"
)
// Feature related
const (
EnvDisableFaceRecognition EnvironmentVariable = "PHOTOVIEW_DISABLE_FACE_RECOGNITION"
EnvDisableVideoEncoding EnvironmentVariable = "PHOTOVIEW_DISABLE_VIDEO_ENCODING"
EnvDisableRawProcessing EnvironmentVariable = "PHOTOVIEW_DISABLE_RAW_PROCESSING"
EnvVideoHardwareAcceleration EnvironmentVariable = "PHOTOVIEW_VIDEO_HARDWARE_ACCELERATION"
)
// GetName returns the name of the environment variable itself
func (v EnvironmentVariable) GetName() string {
return string(v)
}
// GetValue returns the value of the environment
func (v EnvironmentVariable) GetValue() string {
return os.Getenv(string(v))
}
// GetBool returns the environment variable as a boolean (defaults to false if not defined)
func (v EnvironmentVariable) GetBool() bool {
value := strings.ToLower(os.Getenv(string(v)))
trueValues := []string{"1", "true"}
for _, x := range trueValues {
if value == x {
return true
}
}
return false
}
// ShouldServeUI whether or not the "serve ui" option is enabled
func ShouldServeUI() bool {
return EnvServeUI.GetBool()
}
// DevelopmentMode describes whether or not the server is running in development mode,
// and should thus print debug informations and enable other features related to developing.
func DevelopmentMode() bool {
return EnvDevelopmentMode.GetBool()
}
// MediaProbeTimeout returns the media probing timeout duration.
// Defaults to 5 seconds if PHOTOVIEW_MEDIA_PROBE_TIMEOUT is not set or invalid.
// The value is interpreted as seconds.
func MediaProbeTimeout() time.Duration {
if val := EnvMediaProbeTimeout.GetValue(); val != "" {
if seconds, err := strconv.Atoi(val); err == nil && seconds > 0 {
return time.Duration(seconds) * time.Second
}
log.Warn(nil, "Invalid PHOTOVIEW_MEDIA_PROBE_TIMEOUT value, using default 5s", "value", val)
}
return 5 * time.Second
}
// UIPath returns the value from where the static UI files are located if SERVE_UI=1
func UIPath() string {
if path := EnvUIPath.GetValue(); path != "" {
return path
}
return "./ui"
}
+73
View File
@@ -0,0 +1,73 @@
package utils
import (
"os"
"path"
"strconv"
"sync"
"github.com/pkg/errors"
)
// CachePathForMedia is a low-level implementation for Media.CachePath()
func CachePathForMedia(albumID int, mediaID int) (string, error) {
// Make root cache dir if not exists
if _, err := os.Stat(MediaCachePath()); os.IsNotExist(err) {
if err := os.Mkdir(MediaCachePath(), os.ModePerm); err != nil {
return "", errors.Wrap(err, "could not make root image cache directory")
}
}
// Make album cache dir if not exists
albumCachePath := path.Join(MediaCachePath(), strconv.Itoa(int(albumID)))
if _, err := os.Stat(albumCachePath); os.IsNotExist(err) {
if err := os.Mkdir(albumCachePath, os.ModePerm); err != nil {
return "", errors.Wrap(err, "could not make album image cache directory")
}
}
// Make photo cache dir if not exists
photoCachePath := path.Join(albumCachePath, strconv.Itoa(int(mediaID)))
if _, err := os.Stat(photoCachePath); os.IsNotExist(err) {
if err := os.Mkdir(photoCachePath, os.ModePerm); err != nil {
return "", errors.Wrap(err, "could not make photo image cache directory")
}
}
return photoCachePath, nil
}
var (
testCachePath string = ""
testCachePathLocker sync.RWMutex
)
func GetTestCachePath() string {
testCachePathLocker.RLock()
defer testCachePathLocker.RUnlock()
return testCachePath
}
func ConfigureTestCache(tmpDir string) {
testCachePathLocker.Lock()
defer testCachePathLocker.Unlock()
testCachePath = tmpDir
}
// MediaCachePath returns the path for where the media cache is located on the file system
func MediaCachePath() string {
testCachePathLocker.RLock()
cachedPath := testCachePath
testCachePathLocker.RUnlock()
if cachedPath != "" {
return cachedPath
}
photoCache := EnvMediaCachePath.GetValue()
if photoCache == "" {
photoCache = "./media_cache"
}
return photoCache
}
+25
View File
@@ -0,0 +1,25 @@
package utils
import "time"
type Throttle struct {
interval time.Duration
lastAction time.Time
}
func NewThrottle(interval time.Duration) Throttle {
return Throttle{
interval: interval,
lastAction: time.Unix(0, 0),
}
}
func (t *Throttle) Trigger(action func()) {
if action == nil {
return
}
if time.Now().After(t.lastAction.Add(t.interval)) {
t.lastAction = time.Now()
action()
}
}
+92
View File
@@ -0,0 +1,92 @@
package utils
import (
"crypto/rand"
"fmt"
"log"
"math/big"
"os"
"path"
"path/filepath"
)
func GenerateToken() string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
const length = 8
charLen := big.NewInt(int64(len(charset)))
b := make([]byte, length)
for i := range b {
n, err := rand.Int(rand.Reader, charLen)
if err != nil {
log.Panicf("Could not generate random number: %v", err)
}
b[i] = charset[n.Int64()]
}
return string(b)
}
type PhotoviewError struct {
message string
original error
}
func (e PhotoviewError) Error() string {
return fmt.Sprintf("%s: %s", e.message, e.original)
}
func HandleError(message string, err error) PhotoviewError {
log.Printf("ERROR: %s: %s", message, err)
return PhotoviewError{
message: message,
original: err,
}
}
var test_face_recognition_models_path string = ""
func ConfigureTestFaceRecognitionModelsPath(path string) {
test_face_recognition_models_path = path
}
func FaceRecognitionModelsPath() string {
if test_face_recognition_models_path != "" {
return test_face_recognition_models_path
}
if EnvFaceRecognitionModelsPath.GetValue() == "" {
return path.Join("data", "models")
}
return EnvFaceRecognitionModelsPath.GetValue()
}
// IsDirSymlink checks that the given path is a symlink and resolves to a
// directory.
func IsDirSymlink(linkPath string) (bool, error) {
fileInfo, err := os.Lstat(linkPath)
if err != nil {
return false, fmt.Errorf("cannot get fileinfo of the symlink %q: %w", linkPath, err)
}
// Resolve symlinks
if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
resolvedPath, err := filepath.EvalSymlinks(linkPath)
if err != nil {
return false, fmt.Errorf("cannot resolve symlink target for %q, skipping it: %w", linkPath, err)
}
resolvedFile, err := os.Stat(resolvedPath)
if err != nil {
return false, fmt.Errorf("cannot get fileinfo of the symlink %q target %q, skipping it: %w",
linkPath, resolvedPath, err)
}
return resolvedFile.IsDir(), nil
}
return false, nil
}
+76
View File
@@ -0,0 +1,76 @@
package utils_test
import (
"os"
"path"
"testing"
"github.com/photoview/photoview/api/test_utils"
"github.com/photoview/photoview/api/utils"
)
func TestMain(m *testing.M) {
test_utils.IntegrationTestRun(m)
}
func TestIsDirSymlink(t *testing.T) {
test_utils.FilesystemTest(t)
// Prepare a temporary directory for testing purposes
dir, err := os.MkdirTemp("", "testing")
if err != nil {
t.Fatalf("unable to create temp directory for testing")
}
defer os.RemoveAll(dir)
// Create regular file
_, err = os.Create(path.Join(dir, "regular_file"))
if err != nil {
t.Fatalf("unable to create regular file for testing")
}
// Create directory
err = os.Mkdir(path.Join(dir, "directory"), 0755)
if err != nil {
t.Fatalf("unable to create directory for testing")
}
// Create symlink to regular file
err = os.Symlink(path.Join(dir, "regular_file"), path.Join(dir, "file_link"))
if err != nil {
t.Fatalf("unable to create file link for testing")
}
// Create symlink to directory
err = os.Symlink(path.Join(dir, "directory"), path.Join(dir, "dir_link"))
if err != nil {
t.Fatalf("unable to create dir link for testing")
}
// Execute the actual tests
isDirLink, _ := utils.IsDirSymlink(path.Join(dir, "regular_file"))
if isDirLink {
t.Error("Failed detection of regular file")
}
isDirLink, _ = utils.IsDirSymlink(path.Join(dir, "directory"))
if isDirLink {
t.Error("Failed detection of directory")
}
isDirLink, _ = utils.IsDirSymlink(path.Join(dir, "file_link"))
if isDirLink {
t.Error("Failed detection of link to regular file")
}
isDirLink, _ = utils.IsDirSymlink(path.Join(dir, "dir_link"))
if !isDirLink {
t.Error("Failed detection of link to directory")
}
isDirLink, err = utils.IsDirSymlink(path.Join(dir, "non_existant"))
if err == nil {
t.Error("Missing error for non-existant file")
}
}