精简后的项目:仅保留 api/ui 源码、文档、构建依赖
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user