Server: Add "force" and "mode" flags for sockets #4673 #4767 #4765 #4467

These changes allow you to force the re-creation of existing Unix domain
sockets and set the permissions of sockets after they have been created.

The flag or variable value for this must be formatted as follows:
--http-host="unix:/var/run/photoprism.sock?force=true&mode=660"

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-02-04 12:03:00 +01:00
parent 03fcc5d606
commit 1f4f65e988
91 changed files with 376 additions and 88 deletions

View File

@@ -11,8 +11,8 @@ import (
"github.com/photoprism/photoprism/internal/photoprism/get" "github.com/photoprism/photoprism/internal/photoprism/get"
"github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/i18n" "github.com/photoprism/photoprism/pkg/i18n"
"github.com/photoprism/photoprism/pkg/net/header"
) )
//go:embed embed/video.mp4 //go:embed embed/video.mp4

View File

@@ -8,7 +8,7 @@ import (
"github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
) )
// Auth checks if the user is authorized to access a resource with the given permission // Auth checks if the user is authorized to access a resource with the given permission

View File

@@ -10,7 +10,7 @@ import (
"github.com/photoprism/photoprism/internal/auth/acl" "github.com/photoprism/photoprism/internal/auth/acl"
"github.com/photoprism/photoprism/internal/auth/session" "github.com/photoprism/photoprism/internal/auth/session"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
) )
func TestAuth(t *testing.T) { func TestAuth(t *testing.T) {

View File

@@ -3,7 +3,7 @@ package api
import ( import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
) )
// ClientIP returns the client IP address from the request context or a placeholder if it is unknown. // ClientIP returns the client IP address from the request context or a placeholder if it is unknown.

View File

@@ -8,7 +8,7 @@ import (
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/photoprism/get" "github.com/photoprism/photoprism/internal/photoprism/get"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
) )
// AddCountHeader adds the actual result count to the response. // AddCountHeader adds the actual result count to the response.

View File

@@ -16,7 +16,7 @@ import (
"github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/photoprism/get" "github.com/photoprism/photoprism/internal/photoprism/get"
"github.com/photoprism/photoprism/internal/server/limiter" "github.com/photoprism/photoprism/internal/server/limiter"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
) )
type CloseableResponseRecorder struct { type CloseableResponseRecorder struct {

View File

@@ -12,7 +12,7 @@ import (
"github.com/photoprism/photoprism/internal/photoprism/get" "github.com/photoprism/photoprism/internal/photoprism/get"
"github.com/photoprism/photoprism/internal/thumb" "github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
"github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/rnd"
) )

View File

@@ -7,7 +7,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
) )
func TestAddVideoCacheHeader(t *testing.T) { func TestAddVideoCacheHeader(t *testing.T) {

View File

@@ -13,7 +13,7 @@ import (
swagger "github.com/swaggo/gin-swagger" swagger "github.com/swaggo/gin-swagger"
"github.com/photoprism/photoprism/internal/photoprism/get" "github.com/photoprism/photoprism/internal/photoprism/get"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
) )
//go:embed swagger.json //go:embed swagger.json

View File

@@ -8,8 +8,8 @@ import (
"github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/photoprism/get" "github.com/photoprism/photoprism/internal/photoprism/get"
"github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/i18n" "github.com/photoprism/photoprism/pkg/i18n"
"github.com/photoprism/photoprism/pkg/net/header"
) )
// OAuthAuthorize should gather consent and authorization from resource owners when using the // OAuthAuthorize should gather consent and authorization from resource owners when using the

View File

@@ -13,8 +13,8 @@ import (
"github.com/photoprism/photoprism/internal/photoprism/get" "github.com/photoprism/photoprism/internal/photoprism/get"
"github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/i18n" "github.com/photoprism/photoprism/pkg/i18n"
"github.com/photoprism/photoprism/pkg/net/header"
"github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/rnd"
) )

View File

@@ -13,7 +13,7 @@ import (
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
"github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/rnd"
) )

View File

@@ -16,7 +16,7 @@ import (
"github.com/photoprism/photoprism/internal/server/limiter" "github.com/photoprism/photoprism/internal/server/limiter"
"github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
) )
// OAuthToken creates a new access token for clients that authenticate with valid OAuth2 client credentials. // OAuthToken creates a new access token for clients that authenticate with valid OAuth2 client credentials.

View File

@@ -11,7 +11,7 @@ import (
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
) )
func TestOAuthToken(t *testing.T) { func TestOAuthToken(t *testing.T) {

View File

@@ -8,8 +8,8 @@ import (
"github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/photoprism/get" "github.com/photoprism/photoprism/internal/photoprism/get"
"github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/i18n" "github.com/photoprism/photoprism/pkg/i18n"
"github.com/photoprism/photoprism/pkg/net/header"
) )
// OAuthUserinfo should return information about the authenticated user, // OAuthUserinfo should return information about the authenticated user,

View File

@@ -9,8 +9,8 @@ import (
"github.com/photoprism/photoprism/internal/photoprism/get" "github.com/photoprism/photoprism/internal/photoprism/get"
"github.com/photoprism/photoprism/internal/server/limiter" "github.com/photoprism/photoprism/internal/server/limiter"
"github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/i18n" "github.com/photoprism/photoprism/pkg/i18n"
"github.com/photoprism/photoprism/pkg/net/header"
) )
// OIDCLogin redirects a browser to the login page of the configured OpenID Connect provider, if any. // OIDCLogin redirects a browser to the login page of the configured OpenID Connect provider, if any.

View File

@@ -16,8 +16,8 @@ import (
"github.com/photoprism/photoprism/internal/thumb/avatar" "github.com/photoprism/photoprism/internal/thumb/avatar"
"github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/i18n" "github.com/photoprism/photoprism/pkg/i18n"
"github.com/photoprism/photoprism/pkg/net/header"
"github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/rnd"
"github.com/photoprism/photoprism/pkg/time/unix" "github.com/photoprism/photoprism/pkg/time/unix"
"github.com/photoprism/photoprism/pkg/txt" "github.com/photoprism/photoprism/pkg/txt"

View File

@@ -12,7 +12,7 @@ import (
"github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/photoprism/get" "github.com/photoprism/photoprism/internal/photoprism/get"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
"github.com/photoprism/photoprism/pkg/txt" "github.com/photoprism/photoprism/pkg/txt"
) )

View File

@@ -12,8 +12,8 @@ import (
"github.com/photoprism/photoprism/internal/photoprism/get" "github.com/photoprism/photoprism/internal/photoprism/get"
"github.com/photoprism/photoprism/internal/server/limiter" "github.com/photoprism/photoprism/internal/server/limiter"
"github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/i18n" "github.com/photoprism/photoprism/pkg/i18n"
"github.com/photoprism/photoprism/pkg/net/header"
) )
// CreateSession creates a new client session and returns it as JSON if authentication was successful. // CreateSession creates a new client session and returns it as JSON if authentication was successful.

View File

@@ -12,8 +12,8 @@ import (
"github.com/photoprism/photoprism/internal/photoprism/get" "github.com/photoprism/photoprism/internal/photoprism/get"
"github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/i18n" "github.com/photoprism/photoprism/pkg/i18n"
"github.com/photoprism/photoprism/pkg/net/header"
"github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/rnd"
) )

View File

@@ -8,7 +8,7 @@ import (
"github.com/photoprism/photoprism/internal/auth/acl" "github.com/photoprism/photoprism/internal/auth/acl"
"github.com/photoprism/photoprism/internal/photoprism/get" "github.com/photoprism/photoprism/internal/photoprism/get"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
"github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/rnd"
) )

View File

@@ -16,8 +16,8 @@ import (
"github.com/photoprism/photoprism/internal/server/limiter" "github.com/photoprism/photoprism/internal/server/limiter"
"github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/i18n" "github.com/photoprism/photoprism/pkg/i18n"
"github.com/photoprism/photoprism/pkg/net/header"
"github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/rnd"
) )

View File

@@ -13,8 +13,8 @@ import (
"github.com/photoprism/photoprism/internal/photoprism/get" "github.com/photoprism/photoprism/internal/photoprism/get"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/media/video" "github.com/photoprism/photoprism/pkg/media/video"
"github.com/photoprism/photoprism/pkg/net/header"
"github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/rnd"
) )

View File

@@ -9,8 +9,8 @@ import (
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/media/video" "github.com/photoprism/photoprism/pkg/media/video"
"github.com/photoprism/photoprism/pkg/net/header"
) )
func TestGetVideo(t *testing.T) { func TestGetVideo(t *testing.T) {

View File

@@ -145,10 +145,8 @@ func startAction(ctx *cli.Context) error {
auto.Start(conf) auto.Start(conf)
// Wait for signal to initiate server shutdown. // Wait for signal to initiate server shutdown.
quit := make(chan os.Signal) signal.Notify(server.Signal, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1) sig := <-server.Signal
sig := <-quit
// Stop all background activity. // Stop all background activity.
auto.Shutdown() auto.Shutdown()

View File

@@ -35,10 +35,10 @@ func statusAction(ctx *cli.Context) error {
client := &http.Client{Timeout: 10 * time.Second} client := &http.Client{Timeout: 10 * time.Second}
// Connect to unix socket? // Connect to unix socket?
if unixSocket := conf.HttpSocket(); unixSocket != "" { if unixSocket := conf.HttpSocket(); unixSocket != nil {
client.Transport = &http.Transport{ client.Transport = &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return net.Dial("unix", unixSocket) return net.Dial(unixSocket.Scheme, unixSocket.Path)
}, },
} }
} }

View File

@@ -5,7 +5,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
) )
func TestConfig_CdnUrl(t *testing.T) { func TestConfig_CdnUrl(t *testing.T) {

View File

@@ -1,6 +1,7 @@
package config package config
import ( import (
"net/url"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
@@ -8,7 +9,8 @@ import (
"github.com/photoprism/photoprism/internal/config/ttl" "github.com/photoprism/photoprism/internal/config/ttl"
"github.com/photoprism/photoprism/internal/server/limiter" "github.com/photoprism/photoprism/internal/server/limiter"
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
"github.com/photoprism/photoprism/pkg/net/scheme"
) )
const ( const (
@@ -47,13 +49,13 @@ func (c *Config) ProxyProtoHeaders() map[string]string {
h := make(map[string]string, p+1) h := make(map[string]string, p+1)
if p == 0 { if p == 0 {
h[header.ForwardedProto] = header.ProtoHttps h[header.ForwardedProto] = scheme.Https
return h return h
} }
for k, v := range c.options.ProxyProtoHeaders { for k, v := range c.options.ProxyProtoHeaders {
if l := len(c.options.ProxyProtoHttps); l == 0 { if l := len(c.options.ProxyProtoHttps); l == 0 {
h[v] = header.ProtoHttps h[v] = scheme.Https
} else if l > k { } else if l > k {
h[v] = c.options.ProxyProtoHttps[k] h[v] = c.options.ProxyProtoHttps[k]
} else { } else {
@@ -138,16 +140,44 @@ func (c *Config) HttpPort() int {
return c.options.HttpPort return c.options.HttpPort
} }
// HttpSocket tries to parse the HttpHost as a Unix socket path and returns an empty string otherwise. // HttpSocket tries to parse the HttpHost as a Unix socket URL and returns it, or nil if it fails.
func (c *Config) HttpSocket() string { func (c *Config) HttpSocket() *url.URL {
if c.options.HttpSocket != "" { if c.options.HttpSocket != nil {
// Do nothing. // Return cached resource URI.
return c.options.HttpSocket
} else if host := c.options.HttpHost; !strings.HasPrefix(host, "unix:") { } else if host := c.options.HttpHost; !strings.HasPrefix(host, "unix:") {
return "" return nil
} else if strings.Contains(host, "/") {
c.options.HttpSocket = strings.TrimPrefix(host, "unix:")
} }
// Parse socket resource URI.
socket, err := url.Parse(c.options.HttpHost)
// Return nil if parsing failed, or it's not a Unix domain socket URI.
if err != nil {
return nil
}
if socket.Scheme == scheme.HttpUnix {
socket.Scheme = scheme.Unix
}
if socket.Scheme != scheme.Unix || socket.Host == "" && socket.Path == "" {
return nil
} else if socket.Host != "" && socket.Path == "" {
// Create a path from the host if an absolute socket path is not specified,
socket.Path = fs.Abs(socket.Host)
socket.Host = ""
}
// Should never happen.
if socket.Path == "" {
return nil
}
// Cache parsed resource URI.
c.options.HttpSocket = socket
// Return parsed resource URI.
return c.options.HttpSocket return c.options.HttpSocket
} }

View File

@@ -1,11 +1,15 @@
package config package config
import ( import (
"os"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/internal/config/ttl" "github.com/photoprism/photoprism/internal/config/ttl"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/net/scheme"
"github.com/photoprism/photoprism/pkg/txt"
) )
func TestConfig_HttpServerHost(t *testing.T) { func TestConfig_HttpServerHost(t *testing.T) {
@@ -20,10 +24,62 @@ func TestConfig_HttpServerHost(t *testing.T) {
func TestConfig_HttpSocket(t *testing.T) { func TestConfig_HttpSocket(t *testing.T) {
c := NewConfig(CliTestContext()) c := NewConfig(CliTestContext())
assert.Nil(t, c.HttpSocket())
assert.Equal(t, "", c.HttpSocket()) t.Run("Empty", func(t *testing.T) {
c.options.HttpHost = "unix:/tmp/photoprism.sock" c.options.HttpSocket = nil
assert.Equal(t, "/tmp/photoprism.sock", c.HttpSocket()) c.options.HttpHost = ""
result := c.HttpSocket()
assert.Nil(t, result)
})
t.Run("Invalid", func(t *testing.T) {
c.options.HttpSocket = nil
c.options.HttpHost = "unix:http.sock"
result := c.HttpSocket()
assert.Nil(t, result)
})
t.Run("UnixHost", func(t *testing.T) {
c.options.HttpSocket = nil
c.options.HttpHost = "unix://http.sock"
result := c.HttpSocket()
assert.NotNil(t, result)
assert.Equal(t, scheme.Unix, result.Scheme)
assert.Contains(t, result.Path, "/internal/config/http.sock")
assert.False(t, txt.Bool(result.Query().Get("force")))
assert.Equal(t, fs.ModeSocket, fs.ParseMode(result.Query().Get("mode"), fs.ModeSocket))
})
t.Run("UnixPath", func(t *testing.T) {
c.options.HttpSocket = nil
c.options.HttpHost = "unix:/var/run/photoprism.sock?force=false&mode=0640"
result := c.HttpSocket()
assert.NotNil(t, result)
assert.Equal(t, scheme.Unix, result.Scheme)
assert.Equal(t, "/var/run/photoprism.sock", result.Path)
assert.Equal(t, "false", result.Query().Get("force"))
assert.False(t, txt.Bool(result.Query().Get("force")))
assert.Equal(t, os.FileMode(0o640), fs.ParseMode(result.Query().Get("mode"), fs.ModeSocket))
})
t.Run("Force", func(t *testing.T) {
c.options.HttpSocket = nil
c.options.HttpHost = "unix:/tmp/photoprism.sock?force=true&mode=660"
result := c.HttpSocket()
assert.NotNil(t, result)
assert.Equal(t, scheme.Unix, result.Scheme)
assert.Equal(t, "/tmp/photoprism.sock", result.Path)
assert.Equal(t, "true", result.Query().Get("force"))
assert.True(t, txt.Bool(result.Query().Get("force")))
assert.Equal(t, os.FileMode(0o660), fs.ParseMode(result.Query().Get("mode"), 0o000))
})
} }
func TestConfig_HttpServerPort(t *testing.T) { func TestConfig_HttpServerPort(t *testing.T) {

View File

@@ -13,9 +13,10 @@ import (
"github.com/photoprism/photoprism/internal/ffmpeg" "github.com/photoprism/photoprism/internal/ffmpeg"
"github.com/photoprism/photoprism/internal/thumb" "github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/i18n" "github.com/photoprism/photoprism/pkg/i18n"
"github.com/photoprism/photoprism/pkg/media" "github.com/photoprism/photoprism/pkg/media"
"github.com/photoprism/photoprism/pkg/net/header"
"github.com/photoprism/photoprism/pkg/net/scheme"
"github.com/photoprism/photoprism/pkg/txt" "github.com/photoprism/photoprism/pkg/txt"
) )
@@ -616,7 +617,7 @@ var Flags = CliFlags{
Flag: &cli.StringSliceFlag{ Flag: &cli.StringSliceFlag{
Name: "proxy-proto-https", Name: "proxy-proto-https",
Usage: "forwarded HTTPS protocol `NAME`", Usage: "forwarded HTTPS protocol `NAME`",
Value: cli.NewStringSlice(header.ProtoHttps), Value: cli.NewStringSlice(scheme.Https),
EnvVars: EnvVars("PROXY_PROTO_HTTPS"), EnvVars: EnvVars("PROXY_PROTO_HTTPS"),
}}, { }}, {
Flag: &cli.BoolFlag{ Flag: &cli.BoolFlag{
@@ -678,7 +679,7 @@ var Flags = CliFlags{
Name: "http-host", Name: "http-host",
Aliases: []string{"ip"}, Aliases: []string{"ip"},
Value: "0.0.0.0", Value: "0.0.0.0",
Usage: "Web server `IP` address or Unix domain socket, e.g. unix:/var/run/photoprism.sock", Usage: "Web server `IP` address or Unix domain socket, e.g. unix:/var/run/photoprism.sock?force=true&mode=660",
EnvVars: EnvVars("HTTP_HOST"), EnvVars: EnvVars("HTTP_HOST"),
}}, { }}, {
Flag: &cli.IntFlag{ Flag: &cli.IntFlag{

View File

@@ -3,6 +3,7 @@ package config
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/url"
"os" "os"
"time" "time"
@@ -144,7 +145,7 @@ type Options struct {
HttpVideoMaxAge int `yaml:"HttpVideoMaxAge" json:"HttpVideoMaxAge" flag:"http-video-maxage"` HttpVideoMaxAge int `yaml:"HttpVideoMaxAge" json:"HttpVideoMaxAge" flag:"http-video-maxage"`
HttpHost string `yaml:"HttpHost" json:"-" flag:"http-host"` HttpHost string `yaml:"HttpHost" json:"-" flag:"http-host"`
HttpPort int `yaml:"HttpPort" json:"-" flag:"http-port"` HttpPort int `yaml:"HttpPort" json:"-" flag:"http-port"`
HttpSocket string `yaml:"-" json:"-" flag:"-"` HttpSocket *url.URL `yaml:"-" json:"-" flag:"-"`
DatabaseDriver string `yaml:"DatabaseDriver" json:"-" flag:"database-driver"` DatabaseDriver string `yaml:"DatabaseDriver" json:"-" flag:"database-driver"`
DatabaseDsn string `yaml:"DatabaseDsn" json:"-" flag:"database-dsn"` DatabaseDsn string `yaml:"DatabaseDsn" json:"-" flag:"database-dsn"`
DatabaseName string `yaml:"DatabaseName" json:"-" flag:"database-name"` DatabaseName string `yaml:"DatabaseName" json:"-" flag:"database-name"`

View File

@@ -16,9 +16,9 @@ import (
"github.com/photoprism/photoprism/internal/server/limiter" "github.com/photoprism/photoprism/internal/server/limiter"
"github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/i18n" "github.com/photoprism/photoprism/pkg/i18n"
"github.com/photoprism/photoprism/pkg/list" "github.com/photoprism/photoprism/pkg/list"
"github.com/photoprism/photoprism/pkg/net/header"
"github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/rnd"
"github.com/photoprism/photoprism/pkg/time/unix" "github.com/photoprism/photoprism/pkg/time/unix"
"github.com/photoprism/photoprism/pkg/txt" "github.com/photoprism/photoprism/pkg/txt"

View File

@@ -12,8 +12,8 @@ import (
"github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/i18n" "github.com/photoprism/photoprism/pkg/i18n"
"github.com/photoprism/photoprism/pkg/net/header"
"github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/rnd"
"github.com/photoprism/photoprism/pkg/txt" "github.com/photoprism/photoprism/pkg/txt"
) )

View File

@@ -10,7 +10,7 @@ import (
"github.com/photoprism/photoprism/internal/auth/acl" "github.com/photoprism/photoprism/internal/auth/acl"
"github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
"github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/rnd"
"github.com/photoprism/photoprism/pkg/time/unix" "github.com/photoprism/photoprism/pkg/time/unix"
"github.com/photoprism/photoprism/pkg/txt/report" "github.com/photoprism/photoprism/pkg/txt/report"

View File

@@ -4,8 +4,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/photoprism/photoprism/pkg/header"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/internal/ai/face" "github.com/photoprism/photoprism/internal/ai/face"
@@ -14,6 +12,7 @@ import (
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/media/colors" "github.com/photoprism/photoprism/pkg/media/colors"
"github.com/photoprism/photoprism/pkg/media/projection" "github.com/photoprism/photoprism/pkg/media/projection"
"github.com/photoprism/photoprism/pkg/net/header"
"github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/rnd"
) )

View File

@@ -4,13 +4,12 @@ import (
"testing" "testing"
"time" "time"
"github.com/photoprism/photoprism/pkg/header" "github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/pkg/media" "github.com/photoprism/photoprism/pkg/media"
"github.com/photoprism/photoprism/pkg/media/video" "github.com/photoprism/photoprism/pkg/media/video"
"github.com/photoprism/photoprism/pkg/net/header"
"github.com/stretchr/testify/assert"
) )
func TestPhoto_Ids(t *testing.T) { func TestPhoto_Ids(t *testing.T) {

View File

@@ -13,7 +13,7 @@ import (
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
) )
func TestMediaFile_Ok(t *testing.T) { func TestMediaFile_Ok(t *testing.T) {

View File

@@ -6,7 +6,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
) )
// Api is a middleware that sets additional response headers when serving REST API requests. // Api is a middleware that sets additional response headers when serving REST API requests.

View File

@@ -8,7 +8,7 @@ import (
"github.com/photoprism/photoprism/internal/api" "github.com/photoprism/photoprism/internal/api"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
) )
// registerStaticRoutes adds routes for serving static content and templates. // registerStaticRoutes adds routes for serving static content and templates.

View File

@@ -8,7 +8,7 @@ import (
"github.com/photoprism/photoprism/internal/api" "github.com/photoprism/photoprism/internal/api"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
) )
// registerWebAppRoutes adds routes for the web user interface. // registerWebAppRoutes adds routes for the web user interface.

View File

@@ -5,7 +5,7 @@ import (
"github.com/photoprism/photoprism/internal/api" "github.com/photoprism/photoprism/internal/api"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
) )
// Security is a middleware that adds security-related headers to the server's response. // Security is a middleware that adds security-related headers to the server's response.

View File

@@ -0,0 +1,23 @@
package server
import (
"os"
"syscall"
)
// Signal channel for initiating the shutdown.
var Signal = make(chan os.Signal)
// Fail reports an error and shuts down the server.
func Fail(err string, params ...interface{}) {
if err != "" {
log.Errorf(err, params...)
}
Shutdown()
}
// Shutdown gracefully stops the server.
func Shutdown() {
Signal <- syscall.SIGINT
}

View File

@@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
"os"
"time" "time"
"golang.org/x/crypto/acme/autocert" "golang.org/x/crypto/acme/autocert"
@@ -16,7 +17,10 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/net/header"
"github.com/photoprism/photoprism/pkg/txt"
) )
// Start the REST API server using the configuration provided // Start the REST API server using the configuration provided
@@ -97,33 +101,58 @@ func Start(ctx context.Context, conf *config.Config) {
c.String(http.StatusOK, "OK") c.String(http.StatusOK, "OK")
}) })
// Start web server. // Web server configuration.
var tlsErr error var tlsErr error
var tlsManager *autocert.Manager var tlsManager *autocert.Manager
var server *http.Server var server *http.Server
if unixSocket := conf.HttpSocket(); unixSocket != "" { // Listen on a Unix domain socket instead of a TCP port?
if unixSocket := conf.HttpSocket(); unixSocket != nil {
var listener net.Listener var listener net.Listener
var unixAddr *net.UnixAddr var unixAddr *net.UnixAddr
var err error var err error
// Create a Unix socket and attach the server to it. // Check if the Unix socket already exists and delete it if the force flag is set.
if unixAddr, err = net.ResolveUnixAddr("unix", unixSocket); err != nil { if fs.SocketExists(unixSocket.Path) {
log.Errorf("server: invalid unix socket (%s)", err) if txt.Bool(unixSocket.Query().Get("force")) == false {
Fail("server: %s socket %s already exists", clean.Log(unixSocket.Scheme), clean.Log(unixSocket.Path))
return return
} else if listener, err = net.ListenUnix("unix", unixAddr); err != nil { } else if removeErr := os.Remove(unixSocket.Path); removeErr != nil {
log.Errorf("server: failed to listen on unix socket (%s)", err) Fail("server: %s socket %s already exists and cannot be deleted", clean.Log(unixSocket.Scheme), clean.Log(unixSocket.Path))
return
}
}
// Create a Unix socket and listen on it.
if unixAddr, err = net.ResolveUnixAddr(unixSocket.Scheme, unixSocket.Path); err != nil {
Fail("server: invalid %s socket (%s)", clean.Log(unixSocket.Scheme), err)
return
} else if listener, err = net.ListenUnix(unixSocket.Scheme, unixAddr); err != nil {
Fail("server: failed to listen on %s socket (%s)", clean.Log(unixSocket.Scheme), err)
return return
} else { } else {
// Update socket permissions?
if mode := unixSocket.Query().Get("mode"); mode == "" {
// Skip, no socket mode was specified.
} else if modeErr := os.Chmod(unixSocket.Path, fs.ParseMode(mode, fs.ModeSocket)); modeErr != nil {
log.Warnf(
"server: failed to change permissions of %s socket %s (%s)",
clean.Log(unixSocket.Scheme),
clean.Log(unixSocket.Path),
modeErr,
)
}
// Listen on Unix socket, which should be automatically closed and removed after use: // Listen on Unix socket, which should be automatically closed and removed after use:
// https://pkg.go.dev/net#UnixListener.SetUnlinkOnClose. // https://pkg.go.dev/net#UnixListener.SetUnlinkOnClose.
server = &http.Server{ server = &http.Server{
Addr: unixSocket, Addr: listener.Addr().String(),
Handler: router, Handler: router,
} }
log.Infof("server: listening on %s [%s]", unixSocket, time.Since(start)) log.Infof("server: listening on %s [%s]", unixSocket.Path, time.Since(start))
// Start Web server.
go StartHttp(server, listener) go StartHttp(server, listener)
} }
} else if tlsManager, tlsErr = AutoTLS(conf); tlsErr == nil { } else if tlsManager, tlsErr = AutoTLS(conf); tlsErr == nil {
@@ -140,8 +169,10 @@ func Start(ctx context.Context, conf *config.Config) {
} }
log.Infof("server: listening on %s [%s]", server.Addr, time.Since(start)) log.Infof("server: listening on %s [%s]", server.Addr, time.Since(start))
// Start Web server.
go StartAutoTLS(server, tlsManager, conf) go StartAutoTLS(server, tlsManager, conf)
} else if publicCert, privateKey := conf.TLS(); unixSocket == "" && publicCert != "" && privateKey != "" { } else if publicCert, privateKey := conf.TLS(); publicCert != "" && privateKey != "" {
log.Infof("server: starting in tls mode") log.Infof("server: starting in tls mode")
tlsSocket := fmt.Sprintf("%s:%d", conf.HttpHost(), conf.HttpPort()) tlsSocket := fmt.Sprintf("%s:%d", conf.HttpHost(), conf.HttpPort())
@@ -156,6 +187,8 @@ func Start(ctx context.Context, conf *config.Config) {
} }
log.Infof("server: listening on %s [%s]", server.Addr, time.Since(start)) log.Infof("server: listening on %s [%s]", server.Addr, time.Since(start))
// Start Web server.
go StartTLS(server, publicCert, privateKey) go StartTLS(server, publicCert, privateKey)
} else { } else {
log.Infof("server: %s", tlsErr) log.Infof("server: %s", tlsErr)
@@ -163,7 +196,7 @@ func Start(ctx context.Context, conf *config.Config) {
tcpSocket := fmt.Sprintf("%s:%d", conf.HttpHost(), conf.HttpPort()) tcpSocket := fmt.Sprintf("%s:%d", conf.HttpHost(), conf.HttpPort())
if listener, err := net.Listen("tcp", tcpSocket); err != nil { if listener, err := net.Listen("tcp", tcpSocket); err != nil {
log.Errorf("server: %s", err) Fail("server: %s", err)
return return
} else { } else {
server = &http.Server{ server = &http.Server{
@@ -173,6 +206,7 @@ func Start(ctx context.Context, conf *config.Config) {
log.Infof("server: listening on %s [%s]", server.Addr, time.Since(start)) log.Infof("server: listening on %s [%s]", server.Addr, time.Since(start))
// Start Web server.
go StartHttp(server, listener) go StartHttp(server, listener)
} }
} }

View File

@@ -6,7 +6,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
) )
// Static is a middleware that adds static content-related headers to the server's response. // Static is a middleware that adds static content-related headers to the server's response.

View File

@@ -16,7 +16,7 @@ import (
"github.com/photoprism/photoprism/internal/workers/auto" "github.com/photoprism/photoprism/internal/workers/auto"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
"github.com/photoprism/photoprism/pkg/txt" "github.com/photoprism/photoprism/pkg/txt"
) )

View File

@@ -20,7 +20,7 @@ import (
"github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
"github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/rnd"
) )

View File

@@ -7,7 +7,7 @@ import (
"github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/server/limiter" "github.com/photoprism/photoprism/internal/server/limiter"
"github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
"github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/rnd"
) )

View File

@@ -13,7 +13,7 @@ import (
"github.com/photoprism/photoprism/internal/auth/acl" "github.com/photoprism/photoprism/internal/auth/acl"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
"github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/rnd"
) )

View File

@@ -22,7 +22,7 @@ import (
"github.com/photoprism/photoprism/internal/service/hub/places" "github.com/photoprism/photoprism/internal/service/hub/places"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
) )
type Status string type Status string

View File

@@ -9,7 +9,7 @@ import (
"time" "time"
"github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
"github.com/photoprism/photoprism/pkg/txt" "github.com/photoprism/photoprism/pkg/txt"
) )

View File

@@ -3,7 +3,7 @@ package clean
import ( import (
"strings" "strings"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
) )
// ContentType normalizes media content type strings, see https://en.wikipedia.org/wiki/Media_type. // ContentType normalizes media content type strings, see https://en.wikipedia.org/wiki/Media_type.

View File

@@ -42,7 +42,32 @@ const (
HomePath = Home + PathSeparator HomePath = Home + PathSeparator
) )
// FileExists returns true if file exists and is not a directory. // Stat returns the os.FileInfo for the given file path, or an error if it does not exist.
func Stat(filePath string) (os.FileInfo, error) {
if filePath == "" {
return nil, fmt.Errorf("empty filepath")
}
return os.Stat(filePath)
}
// SocketExists returns true if the specified socket exists and is not a regular file or directory.
func SocketExists(socketName string) bool {
if socketName == "" {
return false
}
// Check if path exists and is a socket.
if info, err := os.Stat(socketName); err != nil {
return false
} else if mode := info.Mode(); info.IsDir() || mode.IsRegular() || mode.Type() != os.ModeSocket {
return false
}
return true
}
// FileExists returns true if specified file exists and is not a directory.
func FileExists(fileName string) bool { func FileExists(fileName string) bool {
if fileName == "" { if fileName == "" {
return false return false

View File

@@ -1,10 +1,29 @@
package fs package fs
import "os" import (
"os"
"strconv"
)
// File and directory permissions. // File and directory permissions.
var ( var (
ModeDir os.FileMode = 0o777 ModeDir os.FileMode = 0o777
ModeSocket os.FileMode = 0o666
ModeFile os.FileMode = 0o666 ModeFile os.FileMode = 0o666
ModeBackup os.FileMode = 0o600 ModeBackup os.FileMode = 0o600
) )
// ParseMode parses and returns a filesystem permission mode,
// or the specified default mode if it could not be parsed.
func ParseMode(s string, defaultMode os.FileMode) os.FileMode {
if s == "" {
return defaultMode
}
mode, err := strconv.ParseUint(s, 8, 32)
if err != nil || mode <= 0 {
return defaultMode
}
return os.FileMode(mode)
}

42
pkg/fs/mode_test.go Normal file
View File

@@ -0,0 +1,42 @@
package fs
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseMode(t *testing.T) {
t.Run("Default", func(t *testing.T) {
mode := ParseMode("", ModeSocket)
assert.Equal(t, ModeSocket, mode)
assert.Equal(t, os.FileMode(0o666), mode)
})
t.Run("777", func(t *testing.T) {
mode := ParseMode("777", ModeSocket)
assert.Equal(t, os.ModePerm, mode)
assert.Equal(t, os.FileMode(0o777), mode)
})
t.Run("0777", func(t *testing.T) {
mode := ParseMode("0777", ModeSocket)
assert.Equal(t, os.ModePerm, mode)
assert.Equal(t, os.FileMode(0o777), mode)
})
t.Run("0770", func(t *testing.T) {
mode := ParseMode("0770", ModeSocket)
assert.Equal(t, os.FileMode(0o770), mode)
})
t.Run("0666", func(t *testing.T) {
mode := ParseMode("0666", ModeSocket)
assert.Equal(t, os.FileMode(0o666), mode)
})
t.Run("0660", func(t *testing.T) {
mode := ParseMode("0660", ModeSocket)
assert.Equal(t, os.FileMode(0o660), mode)
})
t.Run("660", func(t *testing.T) {
mode := ParseMode("660", ModeSocket)
assert.Equal(t, os.FileMode(0o660), mode)
})
}

View File

@@ -5,7 +5,7 @@ import (
"strings" "strings"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
) )
// ContentType returns a normalized video content type strings based on the video file type and codec. // ContentType returns a normalized video content type strings based on the video file type and codec.

View File

@@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
) )
func TestContentType(t *testing.T) { func TestContentType(t *testing.T) {

View File

@@ -4,8 +4,8 @@ import (
"time" "time"
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/media" "github.com/photoprism/photoprism/pkg/media"
"github.com/photoprism/photoprism/pkg/net/header"
) )
// Info represents video file information. // Info represents video file information.

View File

@@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/net/header"
) )
func TestInfo(t *testing.T) { func TestInfo(t *testing.T) {

View File

@@ -8,8 +8,8 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/media" "github.com/photoprism/photoprism/pkg/media"
"github.com/photoprism/photoprism/pkg/net/header"
) )
func TestProbeFile(t *testing.T) { func TestProbeFile(t *testing.T) {

8
pkg/net/scheme/http.go Normal file
View File

@@ -0,0 +1,8 @@
package scheme
const (
Http = "http"
Https = "https"
HttpUnix = Http + "+" + Unix
Websocket = "wss"
)

25
pkg/net/scheme/scheme.go Normal file
View File

@@ -0,0 +1,25 @@
/*
Package scheme provides abstractions and naming constants for URI/URL resource strings.
Copyright (c) 2018 - 2025 PhotoPrism UG. All rights reserved.
This program is free software: you can redistribute it and/or modify
it under Version 3 of the GNU Affero General Public License (the "AGPL"):
<https://docs.photoprism.app/license/agpl>
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
The AGPL is supplemented by our Trademark and Brand Guidelines,
which describe how our Brand Assets may be used:
<https://www.photoprism.app/trademark>
Feel free to send an email to hello@photoprism.app if you have questions,
want to support our work, or just want to say hello.
Additional information can be found in our Developer Guide:
<https://docs.photoprism.app/developer-guide/>
*/
package scheme

View File

@@ -0,0 +1,21 @@
package scheme
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestScheme(t *testing.T) {
t.Run("HTTP", func(t *testing.T) {
assert.Equal(t, "http", Http)
assert.Equal(t, "http+unix", HttpUnix)
assert.Equal(t, "https", Https)
assert.Equal(t, "wss", Websocket)
})
t.Run("Unix", func(t *testing.T) {
assert.Equal(t, "unix", Unix)
assert.Equal(t, "unixgram", Unixgram)
assert.Equal(t, "unixpacket", Unixpacket)
})
}

7
pkg/net/scheme/unix.go Normal file
View File

@@ -0,0 +1,7 @@
package scheme
const (
Unix = "unix"
Unixgram = "unixgram"
Unixpacket = "unixpacket"
)