Improved docker and application config

This commit is contained in:
Michael Mayer 2018-09-13 20:54:34 +02:00
parent d63e486499
commit 31562d43cb
19 changed files with 232 additions and 75 deletions

View file

@ -100,8 +100,9 @@ ENV GOPATH /go
ENV GOBIN $GOPATH/bin
ENV PATH $GOBIN:/usr/local/go/bin:$PATH
ENV GO111MODULE on
ENV NODE_ENV production
RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH"
RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" /etc/photoprism /var/photos && chmod -R 777 "$GOPATH"
# Download InceptionV3 model
RUN mkdir -p /model && \
@ -113,11 +114,13 @@ RUN mkdir -p /model && \
WORKDIR "/go/src/github.com/photoprism/photoprism"
COPY . .
RUN cp config.example.yml ~/.photoprism
RUN cp config.prod.yml /etc/photoprism/config.yml
# Build PhotoPrism
RUN make dep js install
RUN cp -r server/assets /etc/photoprism
# Expose HTTP port
EXPOSE 80

View file

@ -15,8 +15,8 @@ install:
build:
$(GOBUILD) cmd/photoprism/photoprism.go
js:
(cd frontend && yarn install)
(cd frontend && npm run build)
(cd frontend && yarn install --prod)
(cd frontend && env NODE_ENV=production npm run build)
start:
$(GORUN) cmd/photoprism/photoprism.go start
migrate-db:

View file

@ -1,11 +1,13 @@
package photoprism
import (
"github.com/gosimple/slug"
"github.com/jinzhu/gorm"
)
type Camera struct {
gorm.Model
CameraSlug string
CameraModel string
CameraType string
CameraNotes string
@ -16,8 +18,11 @@ func NewCamera(modelName string) *Camera {
modelName = "Unknown"
}
cameraSlug := slug.MakeLang(modelName, "en")
result := &Camera{
CameraModel: modelName,
CameraSlug: cameraSlug,
}
return result

View file

@ -15,7 +15,7 @@ func main() {
app := cli.NewApp()
app.Name = "PhotoPrism"
app.Usage = "Digital Photo Archive"
app.Version = "0.2.0"
app.Version = "0.0.0"
app.Flags = globalCliFlags
app.Commands = []cli.Command{
{
@ -29,12 +29,15 @@ func main() {
fmt.Printf("NAME VALUE\n")
fmt.Printf("debug %t\n", conf.Debug)
fmt.Printf("config-file %s\n", conf.ConfigFile)
fmt.Printf("server-ip %s\n", conf.ServerIP)
fmt.Printf("server-port %d\n", conf.ServerPort)
fmt.Printf("server-mode %s\n", conf.ServerMode)
fmt.Printf("server-assets-path %s\n", conf.ServerAssetsPath)
fmt.Printf("darktable-cli %s\n", conf.DarktableCli)
fmt.Printf("originals-path %s\n", conf.OriginalsPath)
fmt.Printf("thumbnails-path %s\n", conf.ThumbnailsPath)
fmt.Printf("import-path %s\n", conf.ImportPath)
fmt.Printf("export-path %s\n", conf.ExportPath)
fmt.Printf("server-assets-path %s\n", conf.ServerAssetsPath)
return nil
},
@ -44,17 +47,17 @@ func main() {
Usage: "Starts web server",
Flags: []cli.Flag{
cli.IntFlag{
Name: "port, p",
Name: "server-port, p",
Usage: "HTTP server port",
Value: 80,
},
cli.StringFlag{
Name: "ip, i",
Name: "server-ip, i",
Usage: "HTTP server IP address (optional)",
Value: "",
},
cli.StringFlag{
Name: "mode, m",
Name: "server-mode, m",
Usage: "debug, release or test",
Value: "",
},
@ -64,13 +67,25 @@ func main() {
conf.SetValuesFromCliContext(context)
if context.IsSet("server-ip") {
conf.ServerIP = context.String("server-ip")
}
if context.IsSet("server-port") {
conf.ServerPort = context.Int("server-port")
}
if context.IsSet("server-mode") {
conf.ServerMode = context.String("server-mode")
}
conf.CreateDirectories()
conf.MigrateDb()
fmt.Printf("Starting web server at port %d...\n", context.Int("port"))
server.Start(context.String("ip"), context.Int("port"), context.String("mode"), conf)
server.Start(conf)
fmt.Println("Done.")
@ -288,37 +303,37 @@ var globalCliFlags = []cli.Flag{
cli.StringFlag{
Name: "config-file, c",
Usage: "config filename",
Value: "~/.photoprism",
Value: "/etc/photoprism/config.yml",
},
cli.StringFlag{
Name: "darktable-cli",
Usage: "darktable CLI",
Value: "/Applications/darktable.app/Contents/MacOS/darktable-cli",
Value: "/usr/bin/darktable-cli",
},
cli.StringFlag{
Name: "originals-path",
Usage: "originals path",
Value: "~/Photos/Originals",
Value: "/var/photoprism/originals",
},
cli.StringFlag{
Name: "thumbnails-path",
Usage: "thumbnails path",
Value: "~/Photos/Thumbnails",
Value: "/var/photoprism/thumbnails",
},
cli.StringFlag{
Name: "import-path",
Usage: "import path",
Value: "~/Photos/Import",
Value: "/var/photoprism/import",
},
cli.StringFlag{
Name: "export-path",
Usage: "export path",
Value: "~/Photos/Export",
Value: "/var/photoprism/export",
},
cli.StringFlag{
Name: "server-assets-path",
Usage: "server assets path for templates, js and css",
Value: "~/Photos/Server",
Value: "/var/photoprism/server",
},
cli.StringFlag{
Name: "database-driver",
@ -328,6 +343,6 @@ var globalCliFlags = []cli.Flag{
cli.StringFlag{
Name: "database-dsn",
Usage: "database data source name (DSN)",
Value: "photoprism:photoprism@tcp(database:3306)/photoprism",
Value: "photoprism:photoprism@tcp(localhost:3306)/photoprism",
},
}

View file

@ -1,8 +1,12 @@
debug: true
darktable-cli: /usr/bin/darktable-cli
originals-path: photos/originals
thumbnails-path: photos/thumbnails
import-path: photos/import
export-path: photos/export
server-ip:
server-mode: debug
server-port: 80
server-assets-path: server/assets
database-driver: mysql
database-dsn: photoprism:photoprism@tcp(database:3306)/photoprism?parseTime=true

View file

@ -17,12 +17,15 @@ import (
type Config struct {
Debug bool
ConfigFile string
ServerIP string
ServerPort int
ServerMode string
ServerAssetsPath string
DarktableCli string
OriginalsPath string
ThumbnailsPath string
ImportPath string
ExportPath string
ServerAssetsPath string
DatabaseDriver string
DatabaseDsn string
db *gorm.DB
@ -43,43 +46,61 @@ func (c *Config) SetValuesFromFile(fileName string) error {
c.ConfigFile = fileName
if OriginalsPath, err := yamlConfig.Get("originals-path"); err == nil {
c.OriginalsPath = GetExpandedFilename(OriginalsPath)
if debug, err := yamlConfig.GetBool("debug"); err == nil {
c.Debug = debug
}
if ThumbnailsPath, err := yamlConfig.Get("thumbnails-path"); err == nil {
c.ThumbnailsPath = GetExpandedFilename(ThumbnailsPath)
if serverIP, err := yamlConfig.Get("server-ip"); err == nil {
c.ServerIP = serverIP
}
if ImportPath, err := yamlConfig.Get("import-path"); err == nil {
c.ImportPath = GetExpandedFilename(ImportPath)
if serverPort, err := yamlConfig.GetInt("server-port"); err == nil {
c.ServerPort = int(serverPort)
}
if ExportPath, err := yamlConfig.Get("export-path"); err == nil {
c.ExportPath = GetExpandedFilename(ExportPath)
if serverMode, err := yamlConfig.Get("server-mode"); err == nil {
c.ServerMode = serverMode
}
if ServerAssetsPath, err := yamlConfig.Get("server-assets-path"); err == nil {
c.ServerAssetsPath = GetExpandedFilename(ServerAssetsPath)
if serverAssetsPath, err := yamlConfig.Get("server-assets-path"); err == nil {
c.ServerAssetsPath = GetExpandedFilename(serverAssetsPath)
}
if DarktableCli, err := yamlConfig.Get("darktable-cli"); err == nil {
c.DarktableCli = GetExpandedFilename(DarktableCli)
if originalsPath, err := yamlConfig.Get("originals-path"); err == nil {
c.OriginalsPath = GetExpandedFilename(originalsPath)
}
if DatabaseDriver, err := yamlConfig.Get("database-driver"); err == nil {
c.DatabaseDriver = DatabaseDriver
if thumbnailsPath, err := yamlConfig.Get("thumbnails-path"); err == nil {
c.ThumbnailsPath = GetExpandedFilename(thumbnailsPath)
}
if DatabaseDsn, err := yamlConfig.Get("database-dsn"); err == nil {
c.DatabaseDsn = DatabaseDsn
if importPath, err := yamlConfig.Get("import-path"); err == nil {
c.ImportPath = GetExpandedFilename(importPath)
}
if exportPath, err := yamlConfig.Get("export-path"); err == nil {
c.ExportPath = GetExpandedFilename(exportPath)
}
if darktableCli, err := yamlConfig.Get("darktable-cli"); err == nil {
c.DarktableCli = GetExpandedFilename(darktableCli)
}
if databaseDriver, err := yamlConfig.Get("database-driver"); err == nil {
c.DatabaseDriver = databaseDriver
}
if databaseDsn, err := yamlConfig.Get("database-dsn"); err == nil {
c.DatabaseDsn = databaseDsn
}
return nil
}
func (c *Config) SetValuesFromCliContext(context *cli.Context) error {
c.Debug = context.GlobalBool("debug")
if context.GlobalBool("debug") {
c.Debug = context.GlobalBool("debug")
}
if context.GlobalIsSet("originals-path") {
c.OriginalsPath = GetExpandedFilename(context.GlobalString("originals-path"))

12
config.osx.yml Normal file
View file

@ -0,0 +1,12 @@
debug: false
darktable-cli: /Applications/darktable.app/Contents/MacOS/darktable-cli
originals-path: ~/Photos/Originals
thumbnails-path: ~/Photos/Thumbnails
import-path: ~/Photos/Import
export-path: ~/Photos/Export
server-ip:
server-mode: release
server-port: 8080
server-assets-path: ~/Photos/Server
database-driver: mysql
database-dsn: photoprism:photoprism@tcp(localhost:3306)/photoprism?parseTime=true

12
config.prod.yml Normal file
View file

@ -0,0 +1,12 @@
debug: false
darktable-cli: /usr/bin/darktable-cli
originals-path: /var/photos/originals
thumbnails-path: /var/photos/thumbnails
import-path: /var/photos/import
export-path: /var/photos/export
server-ip:
server-mode: release
server-port: 80
server-assets-path: /etc/photoprism/assets
database-driver: mysql
database-dsn: photoprism:photoprism@tcp(database:3306)/photoprism?parseTime=true

View file

@ -87,7 +87,7 @@ func TestNewConfig(t *testing.T) {
func TestConfig_SetValuesFromFile(t *testing.T) {
c := NewConfig()
c.SetValuesFromFile(GetExpandedFilename("config.example.yml"))
c.SetValuesFromFile(GetExpandedFilename("config.dev.yml"))
assert.Equal(t, GetExpandedFilename("photos/originals"), c.OriginalsPath)
assert.Equal(t, GetExpandedFilename("photos/thumbnails"), c.ThumbnailsPath)

26
docker-compose.prod.yml Normal file
View file

@ -0,0 +1,26 @@
version: '3.3'
services:
photoprism:
build: .
ports:
- 80:80
volumes:
- photo-data:/var/photos
database:
image: mysql:latest
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=1024
volumes:
- database-data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: photoprism
MYSQL_USER: photoprism
MYSQL_PASSWORD: photoprism
MYSQL_DATABASE: photoprism
volumes:
photo-data:
driver: local
database-data:
driver: local

View file

@ -8,6 +8,7 @@ services:
- 80:80
volumes:
- .:/go/src/github.com/photoprism/photoprism
- ./config.dev.yml:/etc/photoprism/config.yml
database:
image: mysql:latest

View file

@ -23,13 +23,14 @@ Vue.prototype.$config = config;
Vue.use(Vuetify, {
theme: {
primary: '#FDD835',
primary: '#FFD600',
secondary: '#b0bec5',
accent: '#8c9eff',
error: '#F44336',
error: '#E57373',
info: '#00B8D4',
success: '#00BFA5',
warning: '#FFD600',
delete: '#E57373',
},
});

View file

@ -49,6 +49,8 @@
label="Camera"
flat solo
color="blue-grey"
item-value="ID"
item-text="CameraModel"
v-model="query.camera_id"
:items="options.cameras">
</v-select>
@ -121,7 +123,7 @@
fab
dark
small
color="red"
color="delete"
>
<v-icon>delete</v-icon>
</v-btn>
@ -190,16 +192,11 @@
const resultCount = query.hasOwnProperty('count') ? parseInt(query['count']) : 60;
const resultPage = query.hasOwnProperty('page') ? parseInt(query['page']) : 1;
const resultOffset = resultCount * (resultPage - 1);
const order = query.hasOwnProperty('order') && query['order'] != "" ? query['order'] : 'taken_at DESC';
const camera_id = query.hasOwnProperty('camera_id') ? parseInt(query['camera_id']) : '';
const order = query['order'] ? query['order'] : 'taken_at DESC';
const camera_id = query['camera_id'] ? parseInt(query['camera_id']) : 0;
const q = query.hasOwnProperty('q') ? query['q'] : '';
const view = query.hasOwnProperty('view') ? query['view'] : 'tile';
const cameras = [{value: '', text: 'All Cameras'}];
console.log(this.$config.getValue('cameras'));
this.$config.getValue('cameras').forEach(function (camera) {
cameras.push({value: camera.ID, text: camera.CameraModel});
});
const cameras = [{ID: 0, CameraModel: 'All Cameras'}].concat( this.$config.getValue('cameras'));
return {
'snackbarVisible': false,
@ -329,7 +326,7 @@
this.resultCount = parseInt(response.headers['x-result-count']);
this.resultOffset = parseInt(response.headers['x-result-offset']);
this.results = response.models;
this.$alert.info(this.results.length + ' photos found');
this.$alert.info(this.resultTotal + ' photos found');
});
}
},

View file

@ -2,6 +2,8 @@ const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const webpack = require('webpack');
const PATHS = {
app: path.join(__dirname, 'src/app.js'),
css: path.join(__dirname, 'css'),
@ -16,6 +18,7 @@ const cssPlugin = new ExtractTextPlugin({
process.noDeprecation = true;
const config = {
devtool: false,
entry: {
app: PATHS.app,
},
@ -33,7 +36,7 @@ const config = {
},
},
plugins: [
cssPlugin,
cssPlugin
],
node: {
fs: 'empty',
@ -100,8 +103,12 @@ const config = {
};
// No sourcemap for production
if (process.env.NODE_ENV === "production") {
config.devtool = "";
if (process.env.NODE_ENV !== "production") {
const devToolPlugin = new webpack.SourceMapDevToolPlugin({
filename: '[name].map',
});
config.plugins.push(devToolPlugin);
}
module.exports = config;

View file

@ -12,6 +12,10 @@ type Search struct {
db *gorm.DB
}
type SearchCount struct {
Total int
}
type PhotoSearchResult struct {
// Photo
ID uint
@ -72,11 +76,10 @@ func NewSearch(originalsPath string, db *gorm.DB) *Search {
return instance
}
func (s *Search) Photos(form forms.PhotoSearchForm) ([]PhotoSearchResult, error) {
q := s.db.Preload("Tags").Preload("Files").Preload("Location").Preload("Albums")
func (s *Search) Photos(form forms.PhotoSearchForm) ([]PhotoSearchResult, int, error) {
q := s.db.NewScope(nil).DB()
q = q.Table("photos").
Select(`photos.*,
Select(`SQL_CALC_FOUND_ROWS photos.*,
files.id AS file_id, files.file_name, files.file_type, files.file_mime, files.file_width, files.file_height, files.file_aspect_ratio, files.file_orientation,
cameras.camera_model,
locations.loc_display_name, locations.loc_name, locations.loc_city, locations.loc_postcode, locations.loc_country, locations.loc_country_code, locations.loc_category, locations.loc_type,
@ -90,7 +93,7 @@ func (s *Search) Photos(form forms.PhotoSearchForm) ([]PhotoSearchResult, error)
Group("photos.id, files.id")
if form.Query != "" {
q = q.Where("tags.tag_label LIKE ? OR MATCH (photo_title, photo_description, photo_artist, photo_colors) AGAINST (?)", strings.ToLower(form.Query)+"%", form.Query)
q = q.Where("tags.tag_label LIKE ? OR MATCH (photo_title, photo_description, photo_artist, photo_colors) AGAINST (?)", "%"+strings.ToLower(form.Query)+"%", form.Query)
}
if form.CameraID > 0 {
@ -104,7 +107,7 @@ func (s *Search) Photos(form forms.PhotoSearchForm) ([]PhotoSearchResult, error)
rows, err := q.Rows()
if err != nil {
return results, err
return results, 0, err
}
defer rows.Close()
@ -115,7 +118,12 @@ func (s *Search) Photos(form forms.PhotoSearchForm) ([]PhotoSearchResult, error)
results = append(results, result)
}
return results, nil
// TODO: Check if this works properly with concurrent requests and caching
count := &SearchCount{}
s.db.Raw("SELECT FOUND_ROWS() AS total").Scan(&count)
total := count.Total
return results, total, nil
}
func (s *Search) FindFiles(count int, offset int) (files []File) {

View file

@ -5,7 +5,7 @@ import (
"testing"
)
func TestSearch_Photos(t *testing.T) {
func TestSearch_Photos_Query(t *testing.T) {
conf := NewTestConfig()
conf.CreateDirectories()
@ -16,15 +16,52 @@ func TestSearch_Photos(t *testing.T) {
var form forms.PhotoSearchForm
form.Query = "elephant"
form.Query = "african"
form.Count = 3
form.Offset = 0
photos, err := search.Photos(form)
photos, total, err := search.Photos(form)
if err != nil {
t.Fatal(err)
}
t.Log(photos)
t.Logf("Total Count: %d", total)
photos, total, err = search.Photos(form)
if err != nil {
t.Fatal(err)
}
t.Log(photos)
t.Logf("Total Count: %d", total)
}
func TestSearch_Photos_Camera(t *testing.T) {
conf := NewTestConfig()
conf.CreateDirectories()
conf.InitializeTestData(t)
search := NewSearch(conf.OriginalsPath, conf.GetDb())
var form forms.PhotoSearchForm
form.Query = ""
form.CameraID = 2
form.Count = 3
form.Offset = 0
photos, total, err := search.Photos(form)
if err != nil {
t.Fatal(err)
}
t.Log(photos)
t.Logf("Total Count: %d", total)
}

View file

@ -28,15 +28,17 @@ func ConfigureRoutes(app *gin.Engine, conf *photoprism.Config) {
c.MustBindWith(&form, binding.Form)
if photos, err := search.Photos(form); err == nil {
c.Header("x-result-total", strconv.Itoa(len(photos)))
c.Header("x-result-count", strconv.Itoa(form.Count))
c.Header("x-result-offset", strconv.Itoa(form.Offset))
result, total, err := search.Photos(form)
c.JSON(http.StatusOK, photos)
} else {
c.AbortWithError(400, err)
if err != nil {
c.AbortWithStatusJSON(400, gin.H{"error": err.Error()})
}
c.Header("x-result-total", strconv.Itoa(total))
c.Header("x-result-count", strconv.Itoa(form.Count))
c.Header("x-result-offset", strconv.Itoa(form.Offset))
c.JSON(http.StatusOK, result)
})
// v1.OPTIONS()

View file

@ -6,9 +6,9 @@ import (
"github.com/photoprism/photoprism"
)
func Start(address string, port int, mode string, conf *photoprism.Config) {
if mode != "" {
gin.SetMode(mode)
func Start(conf *photoprism.Config) {
if conf.ServerMode != "" {
gin.SetMode(conf.ServerMode)
} else if conf.Debug == false{
gin.SetMode(gin.ReleaseMode)
}
@ -17,5 +17,5 @@ func Start(address string, port int, mode string, conf *photoprism.Config) {
ConfigureRoutes(app, conf)
app.Run(fmt.Sprintf("%s:%d", address, port))
app.Run(fmt.Sprintf("%s:%d", conf.ServerIP, conf.ServerPort))
}

10
util.go
View file

@ -35,9 +35,15 @@ func getRandomInt(min, max int) int {
}
func fileExists(filename string) bool {
_, err := os.Stat(filename)
info, err := os.Stat(filename)
return err == nil
return err == nil && !info.IsDir()
}
func pathExists(pathname string) bool {
info, err := os.Stat(pathname)
return err == nil && info.IsDir()
}
func fileHash(filename string) string {