Refactor download urls and client config

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer
2020-05-27 19:38:40 +02:00
parent 5453cf2e86
commit 6f6e3799dc
59 changed files with 444 additions and 319 deletions

View File

@@ -5,33 +5,33 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ .config.title }}</title>
<title>{{ .config.Title }}</title>
<meta property="og:title" content="{{ .config.title }}: {{ .config.subtitle }}"/>
<meta property="og:image" content="{{ .config.url }}api/v1/preview"/>
<meta property="og:url" content="{{ .config.url }}"/>
<meta property="og:description" content="{{ .config.description }}"/>
<meta property="og:title" content="{{ .config.Title }}: {{ .config.Subtitle }}"/>
<meta property="og:image" content="{{ .config.URL }}api/v1/preview"/>
<meta property="og:url" content="{{ .config.URL }}"/>
<meta property="og:description" content="{{ .config.Description }}"/>
<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:title" content="{{ .config.title }}: {{ .config.subtitle }}"/>
<meta name="twitter:description" content="{{ .config.description }}"/>
<meta name="twitter:image" content="{{ .config.url }}api/v1/preview"/>
<meta name="twitter:title" content="{{ .config.Title }}: {{ .config.Subtitle }}"/>
<meta name="twitter:description" content="{{ .config.Description }}"/>
<meta name="twitter:image" content="{{ .config.URL }}api/v1/preview"/>
<meta name="author" content="{{ .config.author }}">
<meta name="description" content="{{ .config.description }}"/>
<meta name="author" content="{{ .config.Author }}">
<meta name="description" content="{{ .config.Description }}"/>
<link rel="shortcut icon" href="/favicon.ico">
<link rel="apple-touch-icon" href="/static/favicons/favicon.png">
<link rel="icon" type="image/png" href="/static/favicons/favicon.png"/>
<link rel="stylesheet" href="/static/build/app.css?{{ .config.cssHash }}">
<link rel="stylesheet" href="/static/build/app.css?{{ .config.CSSHash }}">
<link rel="manifest" href="/static/manifest.json">
<script>
window.__CONFIG__ = {{ .config }};
</script>
</head>
<body class="{{ .config.flags }}">
<body class="{{ .config.Flags }}">
<!--[if lt IE 8]>
<p class="browserupgrade">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade
your browser</a> to improve your experience.</p>
@@ -44,6 +44,6 @@
<div id="p-busy-overlay"></div>
<script src="/static/build/app.js?{{ .config.jsHash }}"></script>
<script src="/static/build/app.js?{{ .config.JSHash }}"></script>
</body>
</html>

View File

@@ -5,20 +5,20 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ .config.title }}</title>
<title>{{ .config.Title }}</title>
<link rel="shortcut icon" href="/favicon.ico">
<link rel="apple-touch-icon" href="/static/favicons/favicon.png">
<link rel="icon" type="image/png" href="/static/favicons/favicon.png"/>
<link rel="stylesheet" href="/static/build/app.css?{{ .config.cssHash }}">
<link rel="stylesheet" href="/static/build/app.css?{{ .config.CSSHash }}">
<link rel="manifest" href="/static/manifest.json">
<script>
window.__CONFIG__ = {{ .config }};
</script>
</head>
<body class="{{ .config.flags }}">
<body class="{{ .config.Flags }}">
<!--[if lt IE 8]>
<p class="browserupgrade">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade
your browser</a> to improve your experience.</p>
@@ -31,6 +31,6 @@
<div id="p-busy-overlay"></div>
<script src="/static/build/app.js?{{ .config.jsHash }}"></script>
<script src="/static/build/app.js?{{ .config.JSHash }}"></script>
</body>
</html>

View File

@@ -5,34 +5,34 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ .clientConfig.title }}</title>
<title>{{ .config.Title }}</title>
<meta property="og:title" content="{{ .clientConfig.title }}: {{ .clientConfig.subtitle }}"/>
<meta property="og:image" content="{{ .clientConfig.url }}api/v1/preview"/>
<meta property="og:url" content="{{ .clientConfig.url }}"/>
<meta property="og:description" content="{{ .clientConfig.description }}"/>
<meta property="og:title" content="{{ .config.Title }}: {{ .config.Subtitle }}"/>
<meta property="og:image" content="{{ .config.URL }}api/v1/preview"/>
<meta property="og:url" content="{{ .config.URL }}"/>
<meta property="og:description" content="{{ .config.Description }}"/>
<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:title" content="{{ .clientConfig.title }}: {{ .clientConfig.subtitle }}"/>
<meta name="twitter:description" content="{{ .clientConfig.description }}"/>
<meta name="twitter:image" content="{{ .clientConfig.url }}api/v1/preview"/>
<meta name="twitter:title" content="{{ .config.Title }}: {{ .config.Subtitle }}"/>
<meta name="twitter:description" content="{{ .config.Description }}"/>
<meta name="twitter:image" content="{{ .config.URL }}api/v1/preview"/>
<meta name="twitter:site" content="@browseyourlife"/>
<meta name="author" content="{{ .clientConfig.author }}">
<meta name="description" content="{{ .clientConfig.description }}"/>
<meta name="author" content="{{ .config.Author }}">
<meta name="description" content="{{ .config.Description }}"/>
<link rel="shortcut icon" href="/favicon.ico">
<link rel="apple-touch-icon" href="/static/favicons/favicon.png">
<link rel="icon" type="image/png" href="/static/favicons/favicon.png"/>
<link rel="stylesheet" href="/static/build/app.css?{{ .clientConfig.cssHash }}">
<link rel="stylesheet" href="/static/build/app.css?{{ .config.CSSHash }}">
<link rel="manifest" href="/static/manifest.json">
<script>
window.clientConfig = {{ .clientConfig }};
window.__CONFIG__ = {{ .config }};
</script>
</head>
<body class="{{ .clientConfig.flags }}">
<body class="{{ .config.Flags }}">
<!--[if lt IE 8]>
<p class="browserupgrade">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade
your browser</a> to improve your experience.</p>
@@ -45,6 +45,6 @@
<div id="p-busy-overlay"></div>
<script src="/static/build/app.js?{{ .clientConfig.jsHash }}"></script>
<script src="/static/build/app.js?{{ .config.JSHash }}"></script>
</body>
</html>

View File

@@ -1,7 +1,7 @@
import Axios from "axios";
import Notify from "common/notify";
const testConfig = {"jsHash": "test", "version": "test"};
const testConfig = {"jsHash":"48019917", "cssHash":"2b327230", "version": "test"};
const config = window.__CONFIG__ ? window.__CONFIG__ : testConfig;
const Api = Axios.create({

View File

@@ -11,16 +11,23 @@ class Config {
this.storage = storage;
this.storage_key = "config";
this.$vuetify = null;
this.translations = translations;
this.values = values;
this.debug = !!values.debug;
this.page = {
title: "PhotoPrism",
};
this.$vuetify = null;
if(!values) {
console.warn("config: values are empty");
this.debug = true;
this.values = {};
return;
}
Event.subscribe("config.updated", (ev, data) => this.setValues(data));
this.values = values;
this.debug = !!values.debug;
Event.subscribe("config.updated", (ev, data) => this.setValues(data.config));
Event.subscribe("count", (ev, data) => this.onCount(ev, data));
if (this.has("settings")) {
@@ -140,6 +147,14 @@ class Config {
settings() {
return this.values.settings;
}
downloadToken() {
return this.values["downloadToken"];
}
thumbToken() {
return this.values["thumbToken"];
}
}
export default Config;

View File

@@ -54,22 +54,6 @@ class Viewer {
counterEl: false,
arrowEl: true,
preloaderEl: true,
getImageURLForShare: function (button) {
const item = gallery.currItem;
if (!item.original_w) {
button.label = button.template.replace("size", "not available");
return item.download_url;
}
if(button.id === "original") {
button.label = button.template.replace("size", item.original_w + " × " + item.original_h);
return item.download_url;
} else {
button.label = button.template.replace("size", item[button.id].w + " × " + item[button.id].h);
return item[button.id].src + "?download=1";
}
},
addCaptionHTMLFn: function(item, captionEl /*, isFake */) {
// item - slide object
// captionEl - caption DOM element

View File

@@ -110,7 +110,7 @@
return;
}
this.onDownload(`/api/v1/albums/${this.selection[0]}/download`);
this.onDownload(`/api/v1/albums/${this.selection[0]}/dl?t=${this.$config.downloadToken()}`);
this.expanded = false;
},

View File

@@ -106,7 +106,7 @@
},
download() {
Api.post("zip", {"files": this.selection}).then(r => {
this.onDownload("/api/v1/zip/" + r.data.filename);
this.onDownload("/api/v1/zip/" + r.data.filename + "?t=" + this.$config.downloadToken());
});
this.expanded = false;

View File

@@ -133,7 +133,7 @@
return;
}
this.onDownload(`/api/v1/labels/${this.selection[0]}/download`);
this.onDownload(`/api/v1/labels/${this.selection[0]}/dl?t=${this.$config.downloadToken()}`);
this.expanded = false;
},

View File

@@ -186,7 +186,7 @@
downloadFile(index) {
const photo = this.photos[index];
const link = document.createElement('a')
link.href = "/api/v1/download/" + photo.Hash;
link.href = `/api/v1/dl/${photo.Hash}?t=${this.$config.downloadToken()}`;
link.download = photo.FileName;
link.click();
},

View File

@@ -234,10 +234,10 @@
},
download() {
if (this.selection.length === 1) {
this.onDownload(`/api/v1/photos/${this.selection[0]}/download`);
this.onDownload(`/api/v1/photos/${this.selection[0]}/dl?t=${this.$config.downloadToken()}`);
} else {
Api.post("zip", {"photos": this.selection}).then(r => {
this.onDownload("/api/v1/zip/" + r.data.filename);
this.onDownload(`/api/v1/zip/${r.data.filename}?t=${this.$config.downloadToken()}`);
});
}

View File

@@ -139,7 +139,7 @@
downloadFile(index) {
const photo = this.photos[index];
const link = document.createElement('a')
link.href = "/api/v1/download/" + photo.Hash;
link.href = `/api/v1/dl/${photo.Hash}?t=${this.$config.downloadToken()}`;
link.download = photo.FileName;
link.click()
},

View File

@@ -19,7 +19,7 @@
</v-btn>
</td>
<td>
<a :href="'/api/v1/download/' + props.item.Hash" class="secondary-dark--text" target="_blank"
<a :href="'/api/v1/dl/' + props.item.Hash + '?t=' + $config.downloadToken()" class="secondary-dark--text" target="_blank"
v-if="$config.feature('download')">
{{ props.item.Name }}
</a>

View File

@@ -1,6 +1,7 @@
import RestModel from "model/rest";
import Api from "common/api";
import {DateTime} from "luxon";
import {config} from "../session";
export class Album extends RestModel {
getDefaults() {
@@ -44,7 +45,7 @@ export class Album extends RestModel {
}
thumbnailUrl(type) {
return "/api/v1/albums/" + this.getId() + "/thumbnail/" + type;
return `/api/v1/albums/${this.getId()}/t/${config.thumbToken()}/${type}`;
}
thumbnailSrcset() {

View File

@@ -2,6 +2,7 @@ import RestModel from "model/rest";
import Api from "common/api";
import {DateTime} from "luxon";
import Util from "common/util";
import {config} from "../session";
export class File extends RestModel {
getDefaults() {
@@ -79,11 +80,11 @@ export class File extends RestModel {
return "/api/v1/svg/raw";
}
return "/api/v1/thumbnails/" + this.Hash + "/" + type;
return `/api/v1/t/${this.Hash}/t/${config.thumbToken()}/${type}`;
}
getDownloadUrl() {
return "/api/v1/download/" + this.Hash;
return "/api/v1/dl/" + this.Hash + "?t=" + config.downloadToken();
}
thumbnailSrcset() {

View File

@@ -1,6 +1,7 @@
import RestModel from "model/rest";
import Api from "common/api";
import {DateTime} from "luxon";
import {config} from "../session";
export class Label extends RestModel {
getDefaults() {
@@ -35,7 +36,7 @@ export class Label extends RestModel {
}
thumbnailUrl(type) {
return "/api/v1/labels/" + this.getId() + "/thumbnail/" + type;
return `/api/v1/labels/${this.getId()}/t/${config.thumbToken()}/${type}`;
}
thumbnailSrcset() {

View File

@@ -2,6 +2,7 @@ import RestModel from "model/rest";
import Api from "common/api";
import {DateTime} from "luxon";
import Util from "common/util";
import {config} from "../session";
export const SrcManual = "manual";
export const CodecAvc1 = "avc1";
@@ -166,7 +167,7 @@ export class Photo extends RestModel {
return "";
}
return "/api/v1/videos/" + file.Hash + "/" + TypeMP4;
return `/api/v1/videos/${file.Hash}/${config.thumbToken()}/${TypeMP4}`;
}
mainFile() {
@@ -204,23 +205,23 @@ export class Photo extends RestModel {
let video = this.videoFile();
if (video && video.Hash) {
return "/api/v1/thumbnails/" + video.Hash + "/" + type;
return `/api/v1/t/${video.Hash}/${config.thumbToken()}/${type}`;
}
return "/api/v1/svg/photo";
}
return "/api/v1/thumbnails/" + hash + "/" + type;
return `/api/v1/t/${hash}/${config.thumbToken()}/${type}`;
}
getDownloadUrl() {
return "/api/v1/download/" + this.mainFileHash();
return `/api/v1/dl/${this.mainFileHash()}?t=${config.downloadToken()}`;
}
downloadAll() {
if (!this.Files) {
let link = document.createElement('a')
link.href = "/api/v1/download/" + this.mainFileHash();
let link = document.createElement("a");
link.href = `/api/v1/dl/${this.mainFileHash()}?t=${config.downloadToken()}`;
link.download = this.baseName(false);
link.click();
return;
@@ -228,12 +229,12 @@ export class Photo extends RestModel {
this.Files.forEach((file) => {
if (!file || !file.Hash) {
console.warn("no file hash found for download", file)
return
console.warn("no file hash found for download", file);
return;
}
let link = document.createElement('a')
link.href = "/api/v1/download/" + file.Hash;
let link = document.createElement("a");
link.href = `/api/v1/dl/${file.Hash}?t=${config.downloadToken()}`;
link.download = this.fileBase(file.Name);
link.click();
});

View File

@@ -1,5 +1,6 @@
import Model from "./model";
import Api from "../common/api";
import {config} from "../session";
const thumbs = window.__CONFIG__.thumbnails;
@@ -176,7 +177,7 @@ export class Thumb extends Model {
}
return "/api/v1/thumbnails/" + file.Hash + "/" + type;
return `/api/v1/t/${file.Hash}/${config.thumbToken()}/${type}`;
}
static downloadUrl(file) {
@@ -184,7 +185,7 @@ export class Thumb extends Model {
return "";
}
return "/api/v1/download/" + file.Hash;
return `/api/v1/dl/${file.Hash}?t=${config.downloadToken()}`;
}
}

View File

@@ -92,7 +92,7 @@
uid: uid,
results: [],
scrollDisabled: true,
pageSize: 480,
pageSize: 120,
offset: 0,
page: 0,
selection: this.$clipboard.selection,

View File

@@ -223,7 +223,7 @@
downloadFile(index) {
const model = this.results[index];
const link = document.createElement('a')
link.href = "/api/v1/download/" + model.Hash;
link.href = `/api/v1/dl/${model.Hash}?t=${this.$config.downloadToken()}`;
link.download = model.Name;
link.click()
},

View File

@@ -90,11 +90,11 @@
const settings = this.$config.settings();
if (settings.features.private) {
if (settings && settings.features.private) {
filter.public = true;
}
if (settings.features.review && !("quality" in this.staticFilter)) {
if (settings && settings.features.review && (!this.staticFilter || !("quality" in this.staticFilter))) {
filter.quality = 3;
}

View File

@@ -166,13 +166,12 @@
let id = features[i].id;
let marker = this.markers[id];
let token = this.$config.thumbToken();
if (!marker) {
let el = document.createElement('div');
el.className = 'marker';
el.title = props.Title;
el.style.backgroundImage =
'url(/api/v1/thumbnails/' +
props.Hash + '/tile_50)';
el.style.backgroundImage = `url(/api/v1/t/${props.Hash}/${token}/tile_50)`;
el.style.width = '50px';
el.style.height = '50px';

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -37,14 +37,14 @@ describe("model/album", () => {
const values = {id: 5, Title: "Christmas 2019", Slug: "christmas-2019", UID: 66};
const album = new Album(values);
const result = album.thumbnailUrl("xyz");
assert.equal(result, "/api/v1/albums/66/thumbnail/xyz");
assert.equal(result, "/api/v1/albums/66/t/static/xyz");
});
it("should get thumbnail src set", () => {
const values = {id: 5, Title: "Christmas 2019", Slug: "christmas-2019", UID: 66};
const album = new Album(values);
const result = album.thumbnailSrcset("");
assert.equal(result, "/api/v1/albums/66/thumbnail/fit_720 720w, /api/v1/albums/66/thumbnail/fit_1280 1280w, /api/v1/albums/66/thumbnail/fit_1920 1920w, /api/v1/albums/66/thumbnail/fit_2560 2560w, /api/v1/albums/66/thumbnail/fit_3840 3840w");
assert.equal(result, "/api/v1/albums/66/t/static/fit_720 720w, /api/v1/albums/66/t/static/fit_1280 1280w, /api/v1/albums/66/t/static/fit_1920 1920w, /api/v1/albums/66/t/static/fit_2560 2560w, /api/v1/albums/66/t/static/fit_3840 3840w");
});
it("should get thumbnail sizes", () => {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -100,7 +100,7 @@ func CreateAlbum(router *gin.RouterGroup, conf *config.Config) {
event.Success("album created")
event.Publish("config.updated", event.Data(conf.ClientConfig()))
UpdateClientConfig(conf)
PublishAlbumEvent(EntityCreated, m.AlbumUID, c)
@@ -144,7 +144,8 @@ func UpdateAlbum(router *gin.RouterGroup, conf *config.Config) {
return
}
event.Publish("config.updated", event.Data(conf.ClientConfig()))
UpdateClientConfig(conf)
event.Success("album saved")
PublishAlbumEvent(EntityUpdated, uid, c)
@@ -174,7 +175,7 @@ func DeleteAlbum(router *gin.RouterGroup, conf *config.Config) {
conf.Db().Delete(&m)
event.Publish("config.updated", event.Data(conf.ClientConfig()))
UpdateClientConfig(conf)
event.Success(fmt.Sprintf("album %s deleted", txt.Quote(m.AlbumTitle)))
c.JSON(http.StatusOK, m)
@@ -203,7 +204,7 @@ func LikeAlbum(router *gin.RouterGroup, conf *config.Config) {
album.AlbumFavorite = true
conf.Db().Save(&album)
event.Publish("config.updated", event.Data(conf.ClientConfig()))
UpdateClientConfig(conf)
PublishAlbumEvent(EntityUpdated, id, c)
c.JSON(http.StatusOK, http.Response{})
@@ -232,7 +233,7 @@ func DislikeAlbum(router *gin.RouterGroup, conf *config.Config) {
album.AlbumFavorite = false
conf.Db().Save(&album)
event.Publish("config.updated", event.Data(conf.ClientConfig()))
UpdateClientConfig(conf)
PublishAlbumEvent(EntityUpdated, id, c)
c.JSON(http.StatusOK, http.Response{})
@@ -330,9 +331,14 @@ func RemovePhotosFromAlbum(router *gin.RouterGroup, conf *config.Config) {
})
}
// GET /albums/:uid/download
// GET /albums/:uid/dl
func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
router.GET("/albums/:uid/download", func(c *gin.Context) {
router.GET("/albums/:uid/dl", func(c *gin.Context) {
if InvalidDownloadToken(c, conf) {
c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg)
return
}
start := time.Now()
a, err := query.AlbumByUID(c.Param("uid"))
@@ -413,13 +419,18 @@ func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
})
}
// GET /api/v1/albums/:uid/thumbnail/:type
// GET /api/v1/albums/:uid/t/:token/:type
//
// Parameters:
// uid: string Album UID
// type: string Thumbnail type, see photoprism.ThumbnailTypes
func AlbumThumbnail(router *gin.RouterGroup, conf *config.Config) {
router.GET("/albums/:uid/thumbnail/:type", func(c *gin.Context) {
router.GET("/albums/:uid/t/:token/:type", func(c *gin.Context) {
if InvalidToken(c, conf) {
c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg)
return
}
typeName := c.Param("type")
uid := c.Param("uid")
start := time.Now()

View File

@@ -253,7 +253,7 @@ func TestDownloadAlbum(t *testing.T) {
DownloadAlbum(router, conf)
r := PerformRequest(app, "GET", "/api/v1/albums/5678/download")
r := PerformRequest(app, "GET", "/api/v1/albums/5678/dl?t="+conf.DownloadToken())
assert.Equal(t, http.StatusNotFound, r.Code)
})
t.Run("download existing album", func(t *testing.T) {
@@ -261,29 +261,29 @@ func TestDownloadAlbum(t *testing.T) {
DownloadAlbum(router, conf)
r := PerformRequest(app, "GET", "/api/v1/albums/at9lxuqxpogaaba8/download")
r := PerformRequest(app, "GET", "/api/v1/albums/at9lxuqxpogaaba8/dl?t="+conf.DownloadToken())
assert.Equal(t, http.StatusOK, r.Code)
})
}
func TestAlbumThumbnail(t *testing.T) {
t.Run("invalid type", func(t *testing.T) {
app, router, ctx := NewApiTest()
AlbumThumbnail(router, ctx)
r := PerformRequest(app, "GET", "/api/v1/albums/at9lxuqxpogaaba7/thumbnail/xxx")
app, router, conf := NewApiTest()
AlbumThumbnail(router, conf)
r := PerformRequest(app, "GET", "/api/v1/albums/at9lxuqxpogaaba7/t/"+conf.ThumbToken()+"/xxx")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("album has no photo (because is not existing)", func(t *testing.T) {
app, router, ctx := NewApiTest()
AlbumThumbnail(router, ctx)
r := PerformRequest(app, "GET", "/api/v1/albums/987-986435/thumbnail/tile_500")
app, router, conf := NewApiTest()
AlbumThumbnail(router, conf)
r := PerformRequest(app, "GET", "/api/v1/albums/987-986435/t/"+conf.ThumbToken()+"/tile_500")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("album: could not find original", func(t *testing.T) {
app, router, ctx := NewApiTest()
AlbumThumbnail(router, ctx)
r := PerformRequest(app, "GET", "/api/v1/albums/at9lxuqxpogaaba8/thumbnail/tile_500")
app, router, conf := NewApiTest()
AlbumThumbnail(router, conf)
r := PerformRequest(app, "GET", "/api/v1/albums/at9lxuqxpogaaba8/t/"+conf.ThumbToken()+"/tile_500")
assert.Equal(t, http.StatusOK, r.Code)
})
}

View File

@@ -8,6 +8,7 @@ https://github.com/photoprism/photoprism/wiki
package api
import (
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
)
@@ -18,3 +19,7 @@ func report(prefix string, err error) {
log.Errorf("%s: %s", prefix, err.Error())
}
}
func UpdateClientConfig(conf *config.Config) {
event.Publish("config.updated", event.Data{"config": conf.ClientConfig()})
}

View File

@@ -54,7 +54,7 @@ func BatchPhotosArchive(router *gin.RouterGroup, conf *config.Config) {
elapsed := int(time.Since(start).Seconds())
event.Publish("config.updated", event.Data(conf.ClientConfig()))
UpdateClientConfig(conf)
event.EntitiesArchived("photos", f.Photos)
@@ -101,7 +101,7 @@ func BatchPhotosRestore(router *gin.RouterGroup, conf *config.Config) {
elapsed := int(time.Since(start).Seconds())
event.Publish("config.updated", event.Data(conf.ClientConfig()))
UpdateClientConfig(conf)
event.EntitiesRestored("photos", f.Photos)
@@ -135,7 +135,7 @@ func BatchAlbumsDelete(router *gin.RouterGroup, conf *config.Config) {
entity.Db().Where("album_uid IN (?)", f.Albums).Delete(&entity.Album{})
entity.Db().Where("album_uid IN (?)", f.Albums).Delete(&entity.PhotoAlbum{})
event.Publish("config.updated", event.Data(conf.ClientConfig()))
UpdateClientConfig(conf)
event.EntitiesDeleted("albums", f.Albums)
@@ -183,7 +183,7 @@ func BatchPhotosPrivate(router *gin.RouterGroup, conf *config.Config) {
event.EntitiesUpdated("photos", entities)
}
event.Publish("config.updated", event.Data(conf.ClientConfig()))
UpdateClientConfig(conf)
elapsed := time.Since(start)
@@ -216,7 +216,7 @@ func BatchLabelsDelete(router *gin.RouterGroup, conf *config.Config) {
entity.Db().Where("label_uid IN (?)", f.Labels).Delete(&entity.Label{})
event.Publish("config.updated", event.Data(conf.ClientConfig()))
UpdateClientConfig(conf)
event.EntitiesDeleted("labels", f.Labels)

View File

@@ -2,6 +2,7 @@ package api
import (
"fmt"
"net/http"
"path"
"github.com/photoprism/photoprism/internal/config"
@@ -16,12 +17,17 @@ import (
// TODO: GET /api/v1/dl/photo/:uid
// TODO: GET /api/v1/dl/album/:uid
// GET /api/v1/download/:hash
// GET /api/v1/dl/:hash
//
// Parameters:
// hash: string The file hash as returned by the search API
func GetDownload(router *gin.RouterGroup, conf *config.Config) {
router.GET("/download/:hash", func(c *gin.Context) {
router.GET("/dl/:hash", func(c *gin.Context) {
if InvalidDownloadToken(c, conf) {
c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg)
return
}
fileHash := c.Param("hash")
f, err := query.FileByHash(fileHash)

View File

@@ -14,7 +14,7 @@ func TestGetDownload(t *testing.T) {
GetDownload(router, conf)
r := PerformRequest(app, "GET", "/api/v1/download/123xxx")
r := PerformRequest(app, "GET", "/api/v1/dl/123xxx?t="+conf.DownloadToken())
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "record not found", val.String())
assert.Equal(t, http.StatusNotFound, r.Code)
@@ -22,7 +22,7 @@ func TestGetDownload(t *testing.T) {
t.Run("could not find original", func(t *testing.T) {
app, router, conf := NewApiTest()
GetDownload(router, conf)
r := PerformRequest(app, "GET", "/api/v1/download/3cad9168fa6acc5c5c2965ddf6ec465ca42fd818")
r := PerformRequest(app, "GET", "/api/v1/dl/3cad9168fa6acc5c5c2965ddf6ec465ca42fd818?t="+conf.DownloadToken())
assert.Equal(t, http.StatusNotFound, r.Code)
})
}

View File

@@ -80,7 +80,8 @@ func StartImport(router *gin.RouterGroup, conf *config.Config) {
event.Success(fmt.Sprintf("import completed in %d s", elapsed))
event.Publish("import.completed", event.Data{"path": path, "seconds": elapsed})
event.Publish("index.completed", event.Data{"path": path, "seconds": elapsed})
event.Publish("config.updated", event.Data(conf.ClientConfig()))
UpdateClientConfig(conf)
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("import completed in %d s", elapsed)})
})

View File

@@ -68,7 +68,8 @@ func StartIndexing(router *gin.RouterGroup, conf *config.Config) {
event.Success(fmt.Sprintf("indexing completed in %d s", elapsed))
event.Publish("index.completed", event.Data{"path": path, "seconds": elapsed})
event.Publish("config.updated", event.Data(conf.ClientConfig()))
UpdateClientConfig(conf)
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("indexing completed in %d s", elapsed)})
})

View File

@@ -155,15 +155,18 @@ func DislikeLabel(router *gin.RouterGroup, conf *config.Config) {
})
}
// GET /api/v1/labels/:uid/thumbnail/:type
//
// Example: /api/v1/labels/cheetah/thumbnail/tile_500
// GET /api/v1/labels/:uid/t/:token/:type
//
// Parameters:
// uid: string Label UID
// type: string Thumbnail type, see photoprism.ThumbnailTypes
func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) {
router.GET("/labels/:uid/thumbnail/:type", func(c *gin.Context) {
router.GET("/labels/:uid/t/:token/:type", func(c *gin.Context) {
if InvalidToken(c, conf) {
c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg)
return
}
typeName := c.Param("type")
labelUID := c.Param("uid")
start := time.Now()

View File

@@ -1,10 +1,11 @@
package api
import (
"github.com/tidwall/gjson"
"net/http"
"testing"
"github.com/tidwall/gjson"
"github.com/stretchr/testify/assert"
)
@@ -109,22 +110,22 @@ func TestDislikeLabel(t *testing.T) {
func TestLabelThumbnail(t *testing.T) {
t.Run("invalid type", func(t *testing.T) {
app, router, ctx := NewApiTest()
LabelThumbnail(router, ctx)
r := PerformRequest(app, "GET", "/api/v1/labels/lt9k3pw1wowuy3c2/thumbnail/xxx")
app, router, conf := NewApiTest()
LabelThumbnail(router, conf)
r := PerformRequest(app, "GET", "/api/v1/labels/lt9k3pw1wowuy3c2/t/"+conf.ThumbToken()+"/xxx")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("invalid label", func(t *testing.T) {
app, router, ctx := NewApiTest()
LabelThumbnail(router, ctx)
r := PerformRequest(app, "GET", "/api/v1/labels/xxx/thumbnail/tile_500")
app, router, conf := NewApiTest()
LabelThumbnail(router, conf)
r := PerformRequest(app, "GET", "/api/v1/labels/xxx/t/"+conf.ThumbToken()+"/tile_500")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("could not find original", func(t *testing.T) {
app, router, ctx := NewApiTest()
LabelThumbnail(router, ctx)
r := PerformRequest(app, "GET", "/api/v1/labels/lt9k3pw1wowuy3c3/thumbnail/tile_500")
app, router, conf := NewApiTest()
LabelThumbnail(router, conf)
r := PerformRequest(app, "GET", "/api/v1/labels/lt9k3pw1wowuy3c3/t/"+conf.ThumbToken()+"/tile_500")
assert.Equal(t, http.StatusOK, r.Code)
})
}

View File

@@ -108,12 +108,17 @@ func UpdatePhoto(router *gin.RouterGroup, conf *config.Config) {
})
}
// GET /api/v1/photos/:uid/download
// GET /api/v1/photos/:uid/dl
//
// Parameters:
// uid: string PhotoUID as returned by the API
func GetPhotoDownload(router *gin.RouterGroup, conf *config.Config) {
router.GET("/photos/:uid/download", func(c *gin.Context) {
router.GET("/photos/:uid/dl", func(c *gin.Context) {
if InvalidDownloadToken(c, conf) {
c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg)
return
}
f, err := query.FileByPhotoUID(c.Param("uid"))
if err != nil {

View File

@@ -56,33 +56,33 @@ func TestUpdatePhoto(t *testing.T) {
func TestGetPhotoDownload(t *testing.T) {
t.Run("could not find original", func(t *testing.T) {
app, router, ctx := NewApiTest()
GetPhotoDownload(router, ctx)
r := PerformRequest(app, "GET", "/api/v1/photos/pt9jtdre2lvl0yh7/download")
app, router, conf := NewApiTest()
GetPhotoDownload(router, conf)
r := PerformRequest(app, "GET", "/api/v1/photos/pt9jtdre2lvl0yh7/dl?t="+conf.DownloadToken())
assert.Equal(t, http.StatusNotFound, r.Code)
})
t.Run("not existing photo", func(t *testing.T) {
app, router, ctx := NewApiTest()
GetPhotoDownload(router, ctx)
r := PerformRequest(app, "GET", "/api/v1/photos/xxx/download")
app, router, conf := NewApiTest()
GetPhotoDownload(router, conf)
r := PerformRequest(app, "GET", "/api/v1/photos/xxx/dl?t="+conf.DownloadToken())
assert.Equal(t, http.StatusNotFound, r.Code)
})
}
func TestLikePhoto(t *testing.T) {
t.Run("existing photo", func(t *testing.T) {
app, router, ctx := NewApiTest()
LikePhoto(router, ctx)
app, router, conf := NewApiTest()
LikePhoto(router, conf)
r := PerformRequest(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh9/like")
assert.Equal(t, http.StatusOK, r.Code)
GetPhoto(router, ctx)
GetPhoto(router, conf)
r2 := PerformRequest(app, "GET", "/api/v1/photos/pt9jtdre2lvl0yh9")
val := gjson.Get(r2.Body.String(), "Favorite")
assert.Equal(t, "true", val.String())
})
t.Run("not existing photo", func(t *testing.T) {
app, router, ctx := NewApiTest()
LikePhoto(router, ctx)
app, router, conf := NewApiTest()
LikePhoto(router, conf)
r := PerformRequest(app, "POST", "/api/v1/photos/xxx/like")
assert.Equal(t, http.StatusNotFound, r.Code)
})

View File

@@ -13,13 +13,18 @@ import (
"github.com/photoprism/photoprism/pkg/txt"
)
// GET /api/v1/thumbnails/:hash/:type
// GET /api/v1/t/:hash/:token/:type
//
// Parameters:
// hash: string The file hash as returned by the search API
// type: string Thumbnail type, see photoprism.ThumbnailTypes
func GetThumbnail(router *gin.RouterGroup, conf *config.Config) {
router.GET("/thumbnails/:hash/:type", func(c *gin.Context) {
router.GET("/t/:hash/:token/:type", func(c *gin.Context) {
if InvalidToken(c, conf) {
c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg)
return
}
fileHash := c.Param("hash")
typeName := c.Param("type")

View File

@@ -9,23 +9,23 @@ import (
func TestGetThumbnail(t *testing.T) {
t.Run("invalid type", func(t *testing.T) {
app, router, ctx := NewApiTest()
GetThumbnail(router, ctx)
r := PerformRequest(app, "GET", "/api/v1/thumbnails/1/xxx")
app, router, conf := NewApiTest()
GetThumbnail(router, conf)
r := PerformRequest(app, "GET", "/api/v1/t/1/"+conf.ThumbToken()+"/xxx")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("invalid hash", func(t *testing.T) {
app, router, ctx := NewApiTest()
GetThumbnail(router, ctx)
r := PerformRequest(app, "GET", "/api/v1/thumbnails/1/tile_500")
app, router, conf := NewApiTest()
GetThumbnail(router, conf)
r := PerformRequest(app, "GET", "/api/v1/t/1/"+conf.ThumbToken()+"/tile_500")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("could not find original", func(t *testing.T) {
app, router, ctx := NewApiTest()
GetThumbnail(router, ctx)
r := PerformRequest(app, "GET", "/api/v1/thumbnails/2cad9168fa6acc5c5c2965ddf6ec465ca42fd818/tile_500")
app, router, conf := NewApiTest()
GetThumbnail(router, conf)
r := PerformRequest(app, "GET", "/api/v1/t/2cad9168fa6acc5c5c2965ddf6ec465ca42fd818/"+conf.ThumbToken()+"/tile_500")
assert.Equal(t, http.StatusOK, r.Code)
})
}

View File

@@ -61,3 +61,19 @@ func Unauthorized(c *gin.Context, conf *config.Config) bool {
// Check if session token is valid
return !service.Session().Exists(token)
}
// InvalidToken returns true if the token is invalid.
func InvalidToken(c *gin.Context, conf *config.Config) bool {
token := c.Param("token")
if token == "" {
token = c.Query("t")
}
return conf.InvalidToken(token)
}
// InvalidDownloadToken returns true if the token is invalid.
func InvalidDownloadToken(c *gin.Context, conf *config.Config) bool {
return conf.InvalidDownloadToken(c.Query("t"))
}

View File

@@ -5,7 +5,6 @@ import (
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -43,7 +42,8 @@ func SaveSettings(router *gin.RouterGroup, conf *config.Config) {
return
}
event.Publish("config.updated", event.Data(conf.ClientConfig()))
UpdateClientConfig(conf)
log.Infof("settings saved")
c.JSON(http.StatusOK, s)

View File

@@ -12,13 +12,18 @@ import (
"github.com/photoprism/photoprism/pkg/txt"
)
// GET /api/v1/videos/:hash/:type
// GET /api/v1/videos/:hash/:token/:type
//
// Parameters:
// hash: string The photo or video file hash as returned by the search API
// type: string Video type
func GetVideo(router *gin.RouterGroup, conf *config.Config) {
router.GET("/videos/:hash/:type", func(c *gin.Context) {
router.GET("/videos/:hash/:token/:type", func(c *gin.Context) {
if InvalidToken(c, conf) {
c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg)
return
}
fileHash := c.Param("hash")
typeName := c.Param("type")

View File

@@ -10,25 +10,25 @@ func TestGetVideo(t *testing.T) {
t.Run("invalid hash", func(t *testing.T) {
app, router, conf := NewApiTest()
GetVideo(router, conf)
r := PerformRequest(app, "GET", "/api/v1/videos/xxx/mp4")
r := PerformRequest(app, "GET", "/api/v1/videos/xxx/"+conf.ThumbToken()+"/mp4")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("invalid type", func(t *testing.T) {
app, router, conf := NewApiTest()
GetVideo(router, conf)
r := PerformRequest(app, "GET", "/api/v1/videos/acad9168fa6acc5c5c2965ddf6ec465ca42fd831/xxx")
r := PerformRequest(app, "GET", "/api/v1/videos/acad9168fa6acc5c5c2965ddf6ec465ca42fd831/"+conf.ThumbToken()+"/xxx")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("file for video not found", func(t *testing.T) {
app, router, conf := NewApiTest()
GetVideo(router, conf)
r := PerformRequest(app, "GET", "/api/v1/videos/acad9168fa6acc5c5c2965ddf6ec465ca42fd831/mp4")
r := PerformRequest(app, "GET", "/api/v1/videos/acad9168fa6acc5c5c2965ddf6ec465ca42fd831/"+conf.ThumbToken()+"/mp4")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("file with error", func(t *testing.T) {
app, router, conf := NewApiTest()
GetVideo(router, conf)
r := PerformRequest(app, "GET", "/api/v1/videos/acad9168fa6acc5c5c2965ddf6ec465ca42fd832/mp4")
r := PerformRequest(app, "GET", "/api/v1/videos/acad9168fa6acc5c5c2965ddf6ec465ca42fd832/"+conf.ThumbToken()+"/mp4")
assert.Equal(t, http.StatusOK, r.Code)
})
}

View File

@@ -64,7 +64,7 @@ func wsReader(ws *websocket.Conn, writeMutex *sync.Mutex, connId string, conf *c
writeMutex.Lock()
ws.SetWriteDeadline(time.Now().Add(30 * time.Second))
if err := ws.WriteJSON(gin.H{"event": "config.updated", "data": event.Data(conf.ClientConfig())}); err != nil {
if err := ws.WriteJSON(gin.H{"event": "config.updated", "data": event.Data{"config": conf.ClientConfig()}}); err != nil {
log.Error(err)
}
writeMutex.Unlock()

View File

@@ -109,6 +109,11 @@ func CreateZip(router *gin.RouterGroup, conf *config.Config) {
// GET /api/v1/zip/:filename
func DownloadZip(router *gin.RouterGroup, conf *config.Config) {
router.GET("/zip/:filename", func(c *gin.Context) {
if InvalidDownloadToken(c, conf) {
c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg)
return
}
zipBaseName := filepath.Base(c.Param("filename"))
zipPath := path.Join(conf.TempPath(), "zip")
zipFileName := path.Join(zipPath, zipBaseName)

View File

@@ -52,7 +52,7 @@ func TestDownloadZip(t *testing.T) {
t.Run("zip not existing", func(t *testing.T) {
app, router, conf := NewApiTest()
DownloadZip(router, conf)
r := PerformRequest(app, "GET", "/api/v1/zip/xxx")
r := PerformRequest(app, "GET", "/api/v1/zip/xxx?t="+conf.DownloadToken())
assert.Equal(t, http.StatusNotFound, r.Code)
})
}

View File

@@ -101,6 +101,8 @@ func configAction(ctx *cli.Context) error {
fmt.Printf("%-25s %s\n", "geocoding-api", conf.GeoCodingApi())
// Thumbnails
fmt.Printf("%-25s %s\n", "download-token", conf.DownloadToken())
fmt.Printf("%-25s %s\n", "thumb-token", conf.ThumbToken())
fmt.Printf("%-25s %s\n", "thumb-filter", conf.ThumbFilter())
fmt.Printf("%-25s %t\n", "thumb-uncached", conf.ThumbUncached())
fmt.Printf("%-25s %d\n", "thumb-size", conf.ThumbSize())

View File

@@ -3,6 +3,7 @@ package config
import (
"regexp"
"github.com/photoprism/photoprism/pkg/rnd"
"golang.org/x/crypto/bcrypt"
)
@@ -25,3 +26,31 @@ func (c *Config) CheckPassword(p string) bool {
return ap == p
}
// InvalidDownloadToken returns true if the token is invalid.
func (c *Config) InvalidDownloadToken(t string) bool {
return c.DownloadToken() != t
}
// DownloadToken returns the DOWNLOAD api token (you can optionally use a static value for permanent caching).
func (c *Config) DownloadToken() string {
if c.params.DownloadToken == "" {
c.params.DownloadToken = rnd.Token(8)
}
return c.params.DownloadToken
}
// InvalidToken returns true if the token is invalid.
func (c *Config) InvalidToken(t string) bool {
return c.ThumbToken() != t && c.DownloadToken() != t
}
// ThumbToken returns the THUMBNAILS api token (you can optionally use a static value for permanent caching).
func (c *Config) ThumbToken() string {
if c.params.ThumbToken == "" {
c.params.ThumbToken = rnd.Token(8)
}
return c.params.ThumbToken
}

View File

@@ -12,7 +12,73 @@ import (
)
// ClientConfig contains HTTP client / Web UI config values
type ClientConfig map[string]interface{}
type ClientConfig struct {
Flags string `json:"flags"`
Name string `json:"name"`
URL string `json:"url"`
Title string `json:"title"`
Subtitle string `json:"subtitle"`
Description string `json:"description"`
Author string `json:"author"`
Version string `json:"version"`
Copyright string `json:"copyright"`
Debug bool `json:"debug"`
ReadOnly bool `json:"readonly"`
UploadNSFW bool `json:"uploadNSFW"`
Public bool `json:"public"`
Experimental bool `json:"experimental"`
DisableSettings bool `json:"disableSettings"`
Albums []entity.Album `json:"albums"`
Cameras []entity.Camera `json:"cameras"`
Lenses []entity.Lens `json:"lenses"`
Countries []entity.Country `json:"countries"`
Thumbnails []Thumbnail `json:"thumbnails"`
DownloadToken string `json:"downloadToken"`
ThumbToken string `json:"thumbToken"`
JSHash string `json:"jsHash"`
CSSHash string `json:"cssHash"`
Settings Settings `json:"settings"`
Count ClientCounts `json:"count"`
Pos ClientPosition `json:"pos"`
Years []int `json:"years"`
Colors []map[string]string `json:"colors"`
Categories []CategoryLabel `json:"categories"`
Clip int `json:"clip"`
Server RuntimeInfo `json:"server"`
}
type ClientCounts struct {
Photos uint `json:"photos"`
Videos uint `json:"videos"`
Hidden uint `json:"hidden"`
Favorites uint `json:"favorites"`
Private uint `json:"private"`
Review uint `json:"review"`
Stories uint `json:"stories"`
Albums uint `json:"albums"`
Folders uint `json:"folders"`
Files uint `json:"files"`
Moments uint `json:"moments"`
Countries uint `json:"countries"`
Places uint `json:"places"`
Labels uint `json:"labels"`
LabelMaxPhotos uint `json:"labelMaxPhotos"`
}
type CategoryLabel struct {
LabelUID string `json:"UID"`
CustomSlug string `json:"Slug"`
LabelName string `json:"Name"`
}
type ClientPosition struct {
PhotoUID string `json:"uid"`
LocUID string `json:"loc"`
TakenAt time.Time `json:"utc"`
PhotoLat float64 `json:"lat"`
PhotoLng float64 `json:"lng"`
}
// Flags returns config flags as string slice.
func (c *Config) Flags() (flags []string) {
@@ -45,67 +111,30 @@ func (c *Config) PublicClientConfig() ClientConfig {
return c.ClientConfig()
}
jsHash := fs.Checksum(c.HttpStaticBuildPath() + "/app.js")
cssHash := fs.Checksum(c.HttpStaticBuildPath() + "/app.css")
configFlags := c.Flags()
var noPos = struct {
PhotoUID string `json:"photo"`
LocUID string `json:"location"`
TakenAt time.Time `json:"utc"`
PhotoLat float64 `json:"lat"`
PhotoLng float64 `json:"lng"`
}{}
var count = struct {
Photos uint `json:"photos"`
Videos uint `json:"videos"`
Hidden uint `json:"hidden"`
Favorites uint `json:"favorites"`
Private uint `json:"private"`
Review uint `json:"review"`
Stories uint `json:"stories"`
Albums uint `json:"albums"`
Folders uint `json:"folders"`
Files uint `json:"files"`
Moments uint `json:"moments"`
Countries uint `json:"countries"`
Places uint `json:"places"`
Labels uint `json:"labels"`
LabelMaxPhotos uint `json:"labelMaxPhotos"`
}{}
settings := c.Settings()
result := ClientConfig{
"settings": c.Settings(),
"flags": strings.Join(configFlags, " "),
"name": c.Name(),
"url": c.Url(),
"title": c.Title(),
"subtitle": c.Subtitle(),
"description": c.Description(),
"author": c.Author(),
"version": c.Version(),
"copyright": c.Copyright(),
"debug": c.Debug(),
"readonly": c.ReadOnly(),
"uploadNSFW": c.UploadNSFW(),
"public": c.Public(),
"experimental": c.Experimental(),
"disableSettings": c.DisableSettings(),
"albums": []string{},
"cameras": []string{},
"lenses": []string{},
"countries": []string{},
"thumbnails": Thumbnails,
"jsHash": jsHash,
"cssHash": cssHash,
"count": count,
"pos": noPos,
"years": []int{},
"colors": colors.All.List(),
"categories": []string{},
"clip": txt.ClipDefault,
"server": RuntimeInfo{},
Settings: Settings{Language: settings.Language, Theme: settings.Theme},
Flags: strings.Join(c.Flags(), " "),
Name: c.Name(),
URL: c.Url(),
Title: c.Title(),
Subtitle: c.Subtitle(),
Description: c.Description(),
Author: c.Author(),
Version: c.Version(),
Copyright: c.Copyright(),
Debug: c.Debug(),
ReadOnly: c.ReadOnly(),
Public: c.Public(),
Experimental: c.Experimental(),
Thumbnails: Thumbnails,
Colors: colors.All.List(),
JSHash: fs.Checksum(c.HttpStaticBuildPath() + "/app.js"),
CSSHash: fs.Checksum(c.HttpStaticBuildPath() + "/app.css"),
Clip: txt.ClipDefault,
ThumbToken: "public",
DownloadToken: "public",
}
return result
@@ -115,27 +144,41 @@ func (c *Config) PublicClientConfig() ClientConfig {
func (c *Config) ClientConfig() ClientConfig {
defer log.Debug(capture.Time(time.Now(), "config: client config created"))
db := c.Db()
var cameras []entity.Camera
var lenses []entity.Lens
var albums []entity.Album
var countries []entity.Country
var position struct {
PhotoUID string `json:"uid"`
LocUID string `json:"loc"`
TakenAt time.Time `json:"utc"`
PhotoLat float64 `json:"lat"`
PhotoLng float64 `json:"lng"`
result := ClientConfig{
Settings: *c.Settings(),
Flags: strings.Join(c.Flags(), " "),
Name: c.Name(),
URL: c.Url(),
Title: c.Title(),
Subtitle: c.Subtitle(),
Description: c.Description(),
Author: c.Author(),
Version: c.Version(),
Copyright: c.Copyright(),
Debug: c.Debug(),
ReadOnly: c.ReadOnly(),
UploadNSFW: c.UploadNSFW(),
DisableSettings: c.DisableSettings(),
Public: c.Public(),
Experimental: c.Experimental(),
Colors: colors.All.List(),
Thumbnails: Thumbnails,
DownloadToken: c.DownloadToken(),
ThumbToken: c.ThumbToken(),
JSHash: fs.Checksum(c.HttpStaticBuildPath() + "/app.js"),
CSSHash: fs.Checksum(c.HttpStaticBuildPath() + "/app.css"),
Clip: txt.ClipDefault,
Server: NewRuntimeInfo(),
}
db := c.Db()
db.Table("photos").
Select("photo_uid, loc_uid, photo_lat, photo_lng, taken_at").
Where("deleted_at IS NULL AND photo_lat != 0 AND photo_lng != 0").
Order("taken_at DESC").
Limit(1).Offset(0).
Take(&position)
Take(&result.Pos)
var count = struct {
Photos uint `json:"photos"`
@@ -158,35 +201,35 @@ func (c *Config) ClientConfig() ClientConfig {
Select("SUM(photo_type = 'video' AND photo_quality >= 0 AND photo_private = 0) AS videos, SUM(photo_type IN ('image','raw','live') AND photo_quality < 3 AND photo_quality >= 0 AND photo_private = 0) AS review, SUM(photo_quality = -1) AS hidden, SUM(photo_type IN ('image','raw','live') AND photo_private = 0 AND photo_quality >= 0) AS photos, SUM(photo_favorite = 1 AND photo_quality >= 0) AS favorites, SUM(photo_private = 1 AND photo_quality >= 0) AS private").
Where("photos.id NOT IN (SELECT photo_id FROM files WHERE file_primary = 1 AND (file_missing = 1 OR file_error <> ''))").
Where("deleted_at IS NULL").
Take(&count)
Take(&result.Count)
db.Table("labels").
Select("MAX(photo_count) as label_max_photos, COUNT(*) AS labels").
Where("photo_count > 0").
Where("deleted_at IS NULL").
Where("(label_priority >= 0 || label_favorite = 1)").
Take(&count)
Take(&result.Count)
db.Table("albums").
Select("SUM(album_type = '') AS albums, SUM(album_type = 'moment') AS moments, SUM(album_type = 'folder') AS folders").
Where("deleted_at IS NULL").
Take(&count)
Take(&result.Count)
db.Table("folders").
Select("COUNT(*) AS folders").
Where("folder_ignore = 0").
Where("deleted_at IS NULL").
Take(&count)
Take(&result.Count)
db.Table("files").
Select("COUNT(*) AS files").
Where("file_missing = 0").
Where("deleted_at IS NULL").
Take(&count)
Take(&result.Count)
db.Table("countries").
Select("(COUNT(*) - 1) AS countries").
Take(&count)
Take(&result.Count)
db.Table("places").
Select("SUM(photo_count > 0) AS places").
@@ -194,34 +237,24 @@ func (c *Config) ClientConfig() ClientConfig {
Take(&count)
db.Order("country_slug").
Find(&countries)
Find(&result.Countries)
db.Where("deleted_at IS NULL").
Limit(10000).Order("camera_slug").
Find(&cameras)
Find(&result.Cameras)
db.Where("deleted_at IS NULL").
Limit(10000).Order("lens_slug").
Find(&lenses)
Find(&result.Lenses)
db.Where("deleted_at IS NULL AND album_favorite = 1").
Limit(20).Order("album_title").
Find(&albums)
var years []int
Find(&result.Albums)
db.Table("photos").
Where("photo_year > 0").
Order("photo_year DESC").
Pluck("DISTINCT photo_year", &years)
type CategoryLabel struct {
LabelUID string `json:"UID"`
CustomSlug string `json:"Slug"`
LabelName string `json:"Name"`
}
var categories []CategoryLabel
Pluck("DISTINCT photo_year", &result.Years)
db.Table("categories").
Select("l.label_uid, l.custom_slug, l.label_name").
@@ -230,44 +263,7 @@ func (c *Config) ClientConfig() ClientConfig {
Group("l.custom_slug").
Order("l.custom_slug").
Limit(1000).Offset(0).
Scan(&categories)
jsHash := fs.Checksum(c.HttpStaticBuildPath() + "/app.js")
cssHash := fs.Checksum(c.HttpStaticBuildPath() + "/app.css")
configFlags := c.Flags()
result := ClientConfig{
"flags": strings.Join(configFlags, " "),
"name": c.Name(),
"url": c.Url(),
"title": c.Title(),
"subtitle": c.Subtitle(),
"description": c.Description(),
"author": c.Author(),
"version": c.Version(),
"copyright": c.Copyright(),
"debug": c.Debug(),
"readonly": c.ReadOnly(),
"uploadNSFW": c.UploadNSFW(),
"public": c.Public(),
"experimental": c.Experimental(),
"disableSettings": c.DisableSettings(),
"albums": albums,
"cameras": cameras,
"lenses": lenses,
"countries": countries,
"thumbnails": Thumbnails,
"jsHash": jsHash,
"cssHash": cssHash,
"settings": c.Settings(),
"count": count,
"pos": position,
"years": years,
"colors": colors.All.List(),
"categories": categories,
"clip": txt.ClipDefault,
"server": NewRuntimeInfo(),
}
Scan(&result.Categories)
return result
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/rnd"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@@ -27,6 +28,7 @@ type Config struct {
cache *gc.Cache
params *Params
settings *Settings
token string
}
func init() {
@@ -60,6 +62,7 @@ func NewConfig(ctx *cli.Context) *Config {
c := &Config{
params: NewParams(ctx),
token: rnd.Token(8),
}
c.initSettings()

View File

@@ -355,17 +355,16 @@ func TestConfig_ClientConfig(t *testing.T) {
c := TestConfig()
cc := c.ClientConfig()
assert.NotEmpty(t, cc)
assert.Contains(t, cc, "name")
assert.Contains(t, cc, "version")
assert.Contains(t, cc, "copyright")
assert.Contains(t, cc, "debug")
assert.Contains(t, cc, "readonly")
assert.Contains(t, cc, "cameras")
assert.Contains(t, cc, "countries")
assert.Contains(t, cc, "thumbnails")
assert.Contains(t, cc, "jsHash")
assert.Contains(t, cc, "cssHash")
assert.IsType(t, ClientConfig{}, cc)
assert.NotEmpty(t, cc.Name)
assert.NotEmpty(t, cc.Version)
assert.NotEmpty(t, cc.Copyright)
assert.NotEmpty(t, cc.Thumbnails)
assert.NotEmpty(t, cc.JSHash)
assert.NotEmpty(t, cc.CSSHash)
assert.Equal(t, true, cc.Debug)
assert.Equal(t, false, cc.ReadOnly)
}
func TestConfig_Workers(t *testing.T) {

View File

@@ -259,6 +259,18 @@ var GlobalFlags = []cli.Flag{
Value: "places",
EnvVar: "PHOTOPRISM_GEOCODING_API",
},
cli.StringFlag{
Name: "download-token",
Usage: "url `TOKEN` for file downloads",
Value: "",
EnvVar: "PHOTOPRISM_DOWNLOAD_TOKEN",
},
cli.StringFlag{
Name: "thumb-token",
Usage: "url `TOKEN` for thumbnails and video streaming",
Value: "static",
EnvVar: "PHOTOPRISM_THUMB_TOKEN",
},
cli.StringFlag{
Name: "thumb-filter, f",
Usage: "resample filter (best to worst: blackman, lanczos, cubic, linear)",

View File

@@ -78,12 +78,14 @@ type Params struct {
DetectNSFW bool `yaml:"detect-nsfw" flag:"detect-nsfw"`
UploadNSFW bool `yaml:"upload-nsfw" flag:"upload-nsfw"`
GeoCodingApi string `yaml:"geocoding-api" flag:"geocoding-api"`
JpegHidden bool `yaml:"jpeg-hidden" flag:"jpeg-hidden"`
JpegQuality int `yaml:"jpeg-quality" flag:"jpeg-quality"`
DownloadToken string `yaml:"download-token" flag:"download-token"`
ThumbToken string `yaml:"thumb-token" flag:"thumb-token"`
ThumbFilter string `yaml:"thumb-filter" flag:"thumb-filter"`
ThumbUncached bool `yaml:"thumb-uncached" flag:"thumb-uncached"`
ThumbSize int `yaml:"thumb-size" flag:"thumb-size"`
ThumbLimit int `yaml:"thumb-limit" flag:"thumb-limit"`
JpegHidden bool `yaml:"jpeg-hidden" flag:"jpeg-hidden"`
JpegQuality int `yaml:"jpeg-quality" flag:"jpeg-quality"`
DisableTensorFlow bool `yaml:"disable-tf" flag:"disable-tf"`
DisableSettings bool `yaml:"disable-settings" flag:"disable-settings"`
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/capture"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/rnd"
"github.com/urfave/cli"
)
@@ -38,6 +39,9 @@ func NewTestParams() *Params {
testDataPath := testDataPath(assetsPath)
c := &Params{
Name: "PhotoPrism",
Version: "0.0.0",
Copyright: "(c) 2018-2020 PhotoPrism.org",
Debug: true,
Public: true,
ReadOnly: false,
@@ -96,7 +100,11 @@ func NewTestConfig() *Config {
testConfigMutex.Lock()
defer testConfigMutex.Unlock()
c := &Config{params: NewTestParams()}
c := &Config{
params: NewTestParams(),
token: rnd.Token(8),
}
c.initSettings()
if err := c.Init(context.Background()); err != nil {

View File

@@ -43,4 +43,3 @@ func TestIsUID(t *testing.T) {
assert.False(t, IsUID("_", '_'))
assert.False(t, IsUID("", '_'))
}