Routing: Prefix frontend UI routes with /library #840 #2466

Also improves migrations and updates the db schema docs.

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2022-10-15 21:54:11 +02:00
parent 3bad6820d7
commit e3bb8b19dd
202 changed files with 3189 additions and 2608 deletions

View File

@@ -444,9 +444,11 @@ latest:
docker-compose -f docker-compose.latest.yml stop photoprism-latest
docker-compose -f docker-compose.latest.yml up -d --wait photoprism-latest
start-latest:
docker-compose -f docker-compose.latest.yml up -d --wait photoprism-latest
docker-compose -f docker-compose.latest.yml up photoprism-latest
stop-latest:
docker-compose -f docker-compose.latest.yml stop photoprism-latest
terminal-latest:
docker-compose -f docker-compose.latest.yml exec photoprism-latest bash
logs-latest:
docker-compose -f docker-compose.latest.yml logs -f photoprism-latest
docker-local: docker-local-bookworm

View File

@@ -80,7 +80,7 @@
}
],
"scope": "{{ .config.BaseUri }}/",
"start_url": "{{ .config.BaseUri }}/",
"start_url": "{{ .config.BaseUri }}/library/",
"display": "{{ .config.AppMode }}",
"theme_color": "#000000",
"background_color": "#000000",

View File

@@ -25,8 +25,8 @@ services:
PHOTOPRISM_AUTH_MODE: "password" # authentication mode (public, password)
## Public server URL incl http:// or https:// and /path, :port is optional
PHOTOPRISM_SITE_URL: "https://latest.localssl.dev/"
PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App"
PHOTOPRISM_SITE_DESCRIPTION: "Open-Source Photo Management"
PHOTOPRISM_SITE_CAPTION: "Latest"
PHOTOPRISM_SITE_DESCRIPTION: "Tags and finds pictures without getting in your way!"
PHOTOPRISM_SITE_AUTHOR: "@photoprism_app"
PHOTOPRISM_DEBUG: "true"
PHOTOPRISM_READONLY: "false"
@@ -60,8 +60,8 @@ services:
TF_CPP_MIN_LOG_LEVEL: 0 # show TensorFlow log messages for development
working_dir: "/photoprism"
volumes:
- "/photoprism/storage"
- "/photoprism/originals"
- "./storage:/photoprism/storage"
- "./storage/originals:/photoprism/originals"
## Join shared "photoprism-develop" network
networks:

View File

@@ -123,7 +123,7 @@ config.load().finally(() => {
const router = new Router({
routes: Routes,
mode: "history",
base: config.baseUri + "/",
base: config.baseUri + "/library/",
saveScrollPosition: true,
scrollBehavior: (to, from, savedPosition) => {
if (savedPosition) {

View File

@@ -27,14 +27,14 @@ import Photos from "pages/photos.vue";
import Albums from "pages/albums.vue";
import AlbumPhotos from "pages/album/photos.vue";
import Places from "pages/places.vue";
import Files from "pages/library/files.vue";
import Errors from "pages/library/errors.vue";
import Browse from "pages/files/browse.vue";
import Errors from "pages/files/errors.vue";
import Labels from "pages/labels.vue";
import People from "pages/people.vue";
import Library from "pages/library.vue";
import Files from "pages/files.vue";
import Settings from "pages/settings.vue";
import Login from "pages/auth/login.vue";
import Connect from "pages/auth/connect.vue";
import Login from "pages/login.vue";
import Connect from "pages/connect.vue";
import Discover from "pages/discover.vue";
import About from "pages/about/about.vue";
import Feedback from "pages/about/feedback.vue";
@@ -60,7 +60,7 @@ export default [
},
{
name: "license",
path: "/about/license",
path: "/license",
component: License,
meta: { title: siteTitle, auth: false },
},
@@ -78,7 +78,7 @@ export default [
},
{
name: "login",
path: "/auth/login",
path: "/login",
component: Login,
meta: { title: siteTitle, auth: false, hideNav: true },
beforeEnter: (to, from, next) => {
@@ -265,20 +265,20 @@ export default [
},
{
name: "files",
path: "/library/files*",
component: Files,
path: "/index/files*",
component: Browse,
meta: { title: $gettext("File Browser"), auth: true },
},
{
name: "hidden",
path: "/library/hidden",
path: "/hidden",
component: Photos,
meta: { title: $gettext("Hidden Files"), auth: true },
props: { staticFilter: { hidden: "true" } },
},
{
name: "errors",
path: "/library/errors",
path: "/errors",
component: Errors,
meta: { title: $gettext("Errors"), auth: true },
},
@@ -319,25 +319,25 @@ export default [
meta: { title: $gettext("People"), auth: true, background: "application-light" },
},
{
name: "library",
path: "/library",
component: Library,
name: "index",
path: "/index",
component: Files,
meta: { title: $gettext("Library"), auth: true, background: "application-light" },
props: { tab: "library-index" },
props: { tab: "index" },
},
{
name: "library_import",
path: "/library/import",
component: Library,
name: "index_import",
path: "/import",
component: Files,
meta: { title: $gettext("Library"), auth: true, background: "application-light" },
props: { tab: "library-import" },
props: { tab: "import" },
},
{
name: "library_logs",
path: "/library/logs",
component: Library,
name: "index_logs",
path: "/logs",
component: Files,
meta: { title: $gettext("Library"), auth: true, background: "application-light" },
props: { tab: "library-logs" },
props: { tab: "logs" },
},
{
name: "settings",
@@ -352,40 +352,17 @@ export default [
props: { tab: "settings-general" },
},
{
name: "settings_library",
path: "/settings/library",
name: "settings_files",
path: "/settings/files",
component: Settings,
meta: {
title: $gettext("Settings"),
auth: true,
admin: true,
settings: true,
background: "application-light",
},
props: { tab: "settings-library" },
},
{
name: "settings_sync",
path: "/settings/sync",
component: Settings,
meta: {
title: $gettext("Settings"),
auth: true,
settings: true,
background: "application-light",
},
props: { tab: "settings-sync" },
},
{
name: "settings_account",
path: "/settings/account",
component: Settings,
meta: {
title: $gettext("Settings"),
auth: true,
settings: true,
background: "application-light",
},
props: { tab: "settings-account" },
props: { tab: "settings-files" },
},
{
name: "settings_advanced",
@@ -400,6 +377,30 @@ export default [
},
props: { tab: "settings-advanced" },
},
{
name: "settings_services",
path: "/settings/services",
component: Settings,
meta: {
title: $gettext("Settings"),
auth: true,
settings: true,
background: "application-light",
},
props: { tab: "settings-services" },
},
{
name: "settings_account",
path: "/settings/account",
component: Settings,
meta: {
title: $gettext("Settings"),
auth: true,
settings: true,
background: "application-light",
},
props: { tab: "settings-account" },
},
{
name: "discover",
path: "/discover",

View File

@@ -546,6 +546,10 @@ export default class Config {
return this.values && this.values.public;
}
isDemo() {
return this.values && this.values.demo;
}
isSponsor() {
if (!this.values || !this.values.sponsor) {
return false;

View File

@@ -220,10 +220,10 @@ export default class Session {
getHome() {
if (this.loginRequired()) {
return "login";
} else if (this.user.Role === "guest") {
return "albums";
} else {
} else if (this.config.allow("photos", "access_library")) {
return "browse";
} else {
return "albums";
}
}

View File

@@ -227,7 +227,7 @@
<v-list-tile :to="{name: 'live'}" class="nav-live" @click.stop="">
<v-list-tile-content>
<v-list-tile-title :class="`p-flex-menuitem menu-item ${rtl ? '--rtl' : ''}`">
<translate>Live</translate>
<translate>Live Photos</translate>
<span v-show="config.count.live > 0"
:class="`nav-count ${rtl ? '--rtl' : ''}`">{{ config.count.live | abbreviateCount }}</span>
</v-list-tile-title>
@@ -387,7 +387,7 @@
</v-list-tile-content>
</v-list-tile>
<v-list-tile v-if="isMini && $config.feature('library')" to="/library" class="nav-library" @click.stop="">
<v-list-tile v-if="isMini && $config.feature('library')" :to="{ name: 'index' }" class="nav-library" @click.stop="">
<v-list-tile-action :title="$gettext('Library')">
<v-icon>camera_roll</v-icon>
</v-list-tile-action>
@@ -401,7 +401,7 @@
<v-list-group v-if="!isMini && $config.feature('library')" prepend-icon="camera_roll" no-action>
<template #activator>
<v-list-tile to="/library" class="nav-library" @click.stop="">
<v-list-tile :to="{ name: 'index' }" class="nav-library" @click.stop="">
<v-list-tile-content>
<v-list-tile-title class="p-flex-menuitem">
<translate key="Library">Library</translate>
@@ -410,7 +410,7 @@
</v-list-tile>
</template>
<v-list-tile v-show="$config.feature('files')" to="/library/files" class="nav-originals" @click.stop="">
<v-list-tile v-show="$config.feature('files')" to="/index/files" class="nav-originals" @click.stop="">
<v-list-tile-content>
<v-list-tile-title :class="`p-flex-menuitem menu-item ${rtl ? '--rtl' : ''}`">
<translate key="Originals">Originals</translate>
@@ -420,7 +420,7 @@
</v-list-tile-content>
</v-list-tile>
<v-list-tile to="/library/hidden" class="nav-hidden" @click.stop="">
<v-list-tile :to="{ name: 'hidden' }" class="nav-hidden" @click.stop="">
<v-list-tile-content>
<v-list-tile-title :class="`p-flex-menuitem menu-item ${rtl ? '--rtl' : ''}`">
<translate key="Hidden">Hidden</translate>
@@ -430,7 +430,7 @@
</v-list-tile-content>
</v-list-tile>
<v-list-tile to="/library/errors" class="nav-errors" @click.stop="">
<v-list-tile :to="{ name: 'errors' }" class="nav-errors" @click.stop="">
<v-list-tile-content>
<v-list-tile-title :class="`p-flex-menuitem menu-item ${rtl ? '--rtl' : ''}`">
<translate key="Errors">Errors</translate>
@@ -440,7 +440,7 @@
</v-list-group>
<template v-if="!config.disable.settings">
<v-list-tile v-if="isMini" v-show="$config.feature('settings')" to="/settings" class="nav-settings" @click.stop="">
<v-list-tile v-if="isMini" v-show="$config.feature('settings')" :to="{ name: 'settings' }" class="nav-settings" @click.stop="">
<v-list-tile-action :title="$gettext('Settings')">
<v-icon>settings</v-icon>
</v-list-tile-action>
@@ -454,7 +454,7 @@
<v-list-group v-else v-show="$config.feature('settings')" prepend-icon="settings" no-action>
<template #activator>
<v-list-tile to="/settings" class="nav-settings" @click.stop="">
<v-list-tile :to="{ name: 'settings' }" class="nav-settings" @click.stop="">
<v-list-tile-content>
<v-list-tile-title>
<translate key="Settings">Settings</translate>
@@ -565,11 +565,11 @@
class="menu-action nav-login">
<v-icon>login</v-icon>
</router-link>
<router-link v-if="auth && !routeName('library') && $config.feature('library') && $config.feature('logs')"
:to="{ name: 'library_logs' }" :title="$gettext('Logs')" class="menu-action nav-logs">
<v-icon>notes</v-icon>
<router-link v-if="auth && !routeName('index') && $config.feature('library') && $config.feature('logs')"
:to="{ name: 'logs' }" :title="$gettext('Logs')" class="menu-action nav-logs">
<v-icon>grading</v-icon>
</router-link>
<router-link v-if="auth && $config.feature('settings') && !routeName('settings')" to="/settings"
<router-link v-if="auth && $config.feature('settings') && !routeName('settings')" :to="{ name: 'settings' }"
:title="$gettext('Settings')" class="menu-action nav-settings">
<v-icon>settings</v-icon>
</router-link>
@@ -605,13 +605,13 @@
</div>
<div v-if="auth && !routeName('files') && $config.feature('files') && $config.feature('library')"
class="menu-action nav-files">
<router-link to="/library/files">
<router-link to="/index/files">
<v-icon>folder</v-icon>
<translate>Files</translate>
</router-link>
</div>
<div v-if="auth && !routeName('library') && $config.feature('library')" class="menu-action nav-library">
<router-link :to="{ name: 'library' }">
<div v-if="auth && !routeName('index') && $config.feature('library')" class="menu-action nav-library">
<router-link :to="{ name: 'index' }">
<v-icon>camera_roll</v-icon>
<translate>Index</translate>
</router-link>

View File

@@ -6,6 +6,10 @@ nav .v-list__tile__title.title {
line-height: normal!important;
}
.v-navigation-drawer>.theme--dark.v-list .v-list__tile__sub-title {
color: #ffffff77;
}
#p-navigation .nav-title {
text-align: left;
/* font-weight: bold; */

View File

@@ -23,9 +23,9 @@ Additional information can be found in our Developer Guide:
*/
import PAccountAddDialog from "dialog/account/add.vue";
import PAccountRemoveDialog from "dialog/account/remove.vue";
import PAccountEditDialog from "dialog/account/edit.vue";
import PAccountAddDialog from "dialog/service/add.vue";
import PAccountRemoveDialog from "dialog/service/remove.vue";
import PAccountEditDialog from "dialog/service/edit.vue";
import PPhotoArchiveDialog from "dialog/photo/archive.vue";
import PPhotoAlbumDialog from "dialog/photo/album.vue";
import PPhotoEditDialog from "dialog/photo/edit.vue";

View File

@@ -66,7 +66,7 @@
</v-dialog>
</template>
<script>
import Account from "model/account";
import Service from "model/service";
import * as options from "options/options";
export default {
@@ -80,7 +80,7 @@ export default {
showPassword: false,
loading: false,
search: null,
model: new Account(),
model: new Service(false),
label: {
cancel: this.$gettext("Cancel"),
confirm: this.$gettext("Connect"),

View File

@@ -19,15 +19,15 @@
<v-layout row wrap>
<v-flex xs12 text-xs-left class="pt-2">
<v-select
v-model="account"
v-model="service"
color="secondary-dark" hide-details hide-no-data
flat
:label="$gettext('Account')"
item-text="AccName"
item-value="ID"
return-object
:disabled="loading || noAccounts"
:items="accounts"
:disabled="loading || noServices"
:items="services"
@change="onChange">
</v-select>
</v-flex>
@@ -41,7 +41,7 @@
:search-input.sync="search"
:items="pathItems"
:loading="loading"
:disabled="loading || noAccounts"
:disabled="loading || noServices"
item-text="abs"
item-value="abs"
:label="$gettext('Folder')"
@@ -52,11 +52,11 @@
<v-btn depressed color="secondary-light" class="action-cancel ml-0 mt-0 mb-0 mr-2" @click.stop="cancel">
<translate>Cancel</translate>
</v-btn>
<v-btn v-if="noAccounts" :disabled="isPublic && !isDemo" color="primary-button" depressed dark
<v-btn v-if="noServices" :disabled="isPublic && !isDemo" color="primary-button" depressed dark
class="action-setup ma-0" @click.stop="setup">
<translate>Setup</translate>
</v-btn>
<v-btn v-else :disabled="noAccounts" color="primary-button" depressed dark
<v-btn v-else :disabled="noServices" color="primary-button" depressed dark
class="action-upload ma-0" @click.stop="confirm">
<translate>Upload</translate>
</v-btn>
@@ -67,7 +67,7 @@
</v-dialog>
</template>
<script>
import Account from "model/account";
import Service from "model/service";
import Selection from "common/selection";
export default {
@@ -87,11 +87,11 @@ export default {
return {
isDemo: this.$config.get("demo"),
isPublic: this.$config.get("public"),
noAccounts: false,
noServices: false,
loading: true,
search: null,
account: {},
accounts: [],
service: {},
services: [],
selection: new Selection({}),
path: "/",
paths: [
@@ -131,7 +131,7 @@ export default {
this.$router.push({name: "settings_sync"});
},
confirm() {
if (this.noAccounts) {
if (this.noServices) {
this.$notify.warn(this.$gettext('No servers configured.'));
return;
} else if (this.loading) {
@@ -140,7 +140,7 @@ export default {
}
this.loading = true;
this.account.Share(this.selection, this.path).then(
this.service.Upload(this.selection, this.path).then(
(files) => {
this.loading = false;
@@ -150,7 +150,7 @@ export default {
this.$notify.success(this.$gettextInterpolate(this.$gettext("%{n} files uploaded"), {n: files.length}));
}
this.$emit('confirm', this.account);
this.$emit('confirm', this.service);
}
).catch(() => this.loading = false);
},
@@ -158,13 +158,13 @@ export default {
this.paths = [{"abs": "/"}];
this.loading = true;
this.account.Folders().then(p => {
this.service.Folders().then(p => {
for (let i = 0; i < p.length; i++) {
this.paths.push(p[i]);
}
this.pathItems = [...this.paths];
this.path = this.account.SharePath;
this.path = this.service.SharePath;
}).finally(() => this.loading = false);
},
load() {
@@ -188,13 +188,13 @@ export default {
offset: 0,
};
Account.search(params).then(response => {
Service.search(params).then(response => {
if (!response.models.length) {
this.noAccounts = true;
this.noServices = true;
this.loading = false;
} else {
this.account = response.models[0];
this.accounts = response.models;
this.service = response.models[0];
this.services = response.models;
this.onChange();
}
}).catch(() => this.loading = false);

View File

@@ -28,7 +28,7 @@ import Api from "common/api";
import { $gettext } from "common/vm";
import { config } from "app/session";
export class Account extends RestModel {
export class Service extends RestModel {
getDefaults() {
return {
ID: 0,
@@ -76,7 +76,7 @@ export class Account extends RestModel {
);
}
Share(selection, folder) {
Upload(selection, folder) {
if (!selection) {
return;
}
@@ -85,13 +85,13 @@ export class Account extends RestModel {
selection = { Photos: selection };
}
return Api.post(this.getEntityResource() + "/share", { selection, folder }).then((response) =>
return Api.post(this.getEntityResource() + "/upload", { selection, folder }).then((response) =>
Promise.resolve(response.data)
);
}
static getCollectionResource() {
return "accounts";
return "services";
}
static getModelName() {
@@ -99,4 +99,4 @@ export class Account extends RestModel {
}
}
export default Account;
export default Service;

View File

@@ -112,28 +112,28 @@ export class User extends RestModel {
return this.Details.NickName;
} else if (this.Details && this.Details.GivenName) {
return this.Details.GivenName;
} else if (this.Name) {
return Util.capitalize(this.Name);
} else if (this.Role) {
return T(Util.capitalize(this.Role));
} else if (this.Details && this.Details.JobTitle) {
return this.Details.JobTitle;
} else if (this.Email) {
return this.Email;
} else if (this.Role) {
return T(Util.capitalize(this.Role));
} else if (this.Name) {
return `@${this.Name}`;
}
return $gettext("Unregistered");
}
getAccountInfo() {
if (this.Email) {
if (this.Name) {
return `@${this.Name}`;
} else if (this.Email) {
return this.Email;
} else if (this.Details && this.Details.JobTitle) {
return this.Details.JobTitle;
} else if (this.Role) {
return T(Util.capitalize(this.Role));
} else if (this.Name) {
return this.Name;
}
return $gettext("Account");

View File

@@ -87,7 +87,7 @@
</p>
<p class="text-xs-center pt-2 ma-0 pb-0">
<router-link to="/about/license">
<router-link to="/license">
<img :src="$config.staticUri + '/img/badge-agpl.svg'" alt="License AGPL v3" style="max-width:100%;"/>
</router-link>
<a target="_blank" href="https://docs.photoprism.app/"><img :src="$config.staticUri + '/img/badge-docs.svg'"

View File

@@ -8,7 +8,7 @@
slider-color="secondary-dark"
:height="$vuetify.breakpoint.smAndDown ? 48 : 64"
>
<v-tab v-for="(item, index) in tabs" :id="'tab-' + item.name" :key="index" :class="item.class" ripple
<v-tab v-for="(item, index) in tabs" :id="'tab-' + item.name" :key="manage" :class="item.class" ripple
@click="changePath(item.path)">
<v-icon v-if="$vuetify.breakpoint.smAndDown" :title="item.label">{{ item.icon }}</v-icon>
<template v-else>
@@ -17,7 +17,7 @@
</v-tab>
<v-tabs-items touchless>
<v-tab-item v-for="(item, index) in tabs" :key="index" lazy>
<v-tab-item v-for="(item, index) in tabs" :key="manage" lazy>
<component :is="item.component"></component>
</v-tab-item>
</v-tabs-items>
@@ -26,9 +26,9 @@
</template>
<script>
import Import from "pages/library/import.vue";
import Index from "pages/library/index.vue";
import Logs from "pages/library/logs.vue";
import Import from "pages/files/import.vue";
import Index from "pages/files/index.vue";
import Logs from "pages/files/logs.vue";
function initTabs(flag, tabs) {
let i = 0;
@@ -58,21 +58,21 @@ export default {
const tabs = [
{
'name': 'library-index',
'name': 'index',
'component': Index,
'label': this.$gettext('Index'),
'class': '',
'path': '/library',
'path': '/index',
'icon': 'camera_roll',
'readonly': true,
'demo': true,
},
{
'name': 'library-import',
'name': 'import',
'component': Import,
'label': this.$gettext('Import'),
'class': '',
'path': '/library/import',
'path': '/import',
'icon': 'create_new_folder',
'readonly': false,
'demo': true,
@@ -81,12 +81,12 @@ export default {
if(this.$config.feature('logs')) {
tabs.push({
'name': 'library-logs',
'name': 'logs',
'component': Logs,
'label': this.$gettext('Logs'),
'class': '',
'path': '/library/logs',
'icon': 'notes',
'path': '/logs',
'icon': 'grading',
'readonly': true,
'demo': true,
});

View File

@@ -3,7 +3,7 @@
<v-form ref="form" class="p-files-search" lazy-validation dense @submit.prevent="updateQuery">
<v-toolbar flat color="secondary" :dense="$vuetify.breakpoint.smAndDown">
<v-toolbar-title>
<router-link to="/library/files">
<router-link to="/index/files">
<translate key="Originals">Originals</translate>
</router-link>
@@ -189,7 +189,7 @@ export default {
methods: {
getBreadcrumbs() {
let result = [];
let path = "/library/files";
let path = "/index/files";
const crumbs = this.path.split("/");
@@ -209,7 +209,7 @@ export default {
// Open Edit Dialog
Event.publish("dialog.edit", {selection: [model.PhotoUID], album: null, index: 0});
} else {
this.$router.push({path: '/library/files/' + model.Path});
this.$router.push({path: '/index/files/' + model.Path});
}
},
downloadFile(index) {

View File

@@ -29,9 +29,9 @@
<script>
import General from "pages/settings/general.vue";
import Library from "pages/settings/library.vue";
import Files from "pages/settings/files.vue";
import Advanced from "pages/settings/advanced.vue";
import Sync from "pages/settings/sync.vue";
import Services from "pages/settings/services.vue";
import Account from "pages/settings/account.vue";
import {config} from "app/session";
@@ -55,8 +55,8 @@ export default {
},
},
data() {
const isDemo = this.$config.get("demo");
const isPublic = this.$config.get("public");
const isDemo = this.$config.isDemo();
const isPublic = this.$config.isPublic();
const tabs = [
{
@@ -72,16 +72,16 @@ export default {
'show': config.feature('settings'),
},
{
'name': 'settings-library',
'component': Library,
'name': 'settings-files',
'component': Files,
'label': this.$gettext('Library'),
'class': '',
'path': '/settings/library',
'path': '/settings/files',
'icon': 'camera_roll',
'public': true,
'admin': true,
'demo': true,
'show': config.feature('advanced'),
'show': config.allow("config", "manage"),
},
{
'name': 'settings-advanced',
@@ -93,19 +93,19 @@ export default {
'public': false,
'admin': true,
'demo': true,
'show': config.feature('advanced'),
'show': config.allow("config", "manage"),
},
{
'name': 'settings-sync',
'component': Sync,
'label': this.$gettext('Sync'),
'name': 'settings-services',
'component': Services,
'label': this.$gettext('Services'),
'class': '',
'path': '/settings/sync',
'path': '/settings/services',
'icon': 'sync_alt',
'public': false,
'admin': true,
'demo': true,
'show': config.feature('sync'),
'show': config.feature('services') && config.allow("services", "manage"),
},
{
'name': 'settings-account',
@@ -113,7 +113,7 @@ export default {
'label': this.$gettext('Account'),
'class': '',
'path': '/settings/account',
'icon': 'person',
'icon': 'admin_panel_settings',
'public': false,
'admin': true,
'demo': true,

View File

@@ -57,7 +57,7 @@
<v-flex xs12 class="pa-2">
<p class="caption pa-0">
<translate>Note: Updating the password will not revoke access from already authenticated users.</translate>
<translate>Note: Updating your password will invalidate other browser sessions, so you will have to log in again.</translate>
</p>
</v-flex>

View File

@@ -46,141 +46,6 @@
<v-card v-if="isDemo || isAdmin" flat tile class="mt-0 px-1 application">
<v-card-actions>
<v-layout wrap align-top>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
v-model="settings.features.upload"
:disabled="busy || config.readonly || isDemo"
class="ma-0 pa-0 input-upload"
color="secondary-dark"
:label="$gettext('Upload')"
:hint="$gettext('Add files to your library via Web Upload.')"
prepend-icon="cloud_upload"
persistent-hint
@change="onChange"
>
</v-checkbox>
</v-flex>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
v-model="settings.features.download"
:disabled="busy || isDemo"
class="ma-0 pa-0 input-download"
color="secondary-dark"
:label="$gettext('Download')"
:hint="$gettext('Download single files and zip archives.')"
prepend-icon="get_app"
persistent-hint
@change="onChange"
>
</v-checkbox>
</v-flex>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
v-model="settings.features.edit"
:disabled="busy || isDemo"
class="ma-0 pa-0 input-edit"
color="secondary-dark"
:label="$gettext('Edit')"
:hint="$gettext('Change photo titles, locations, and other metadata.')"
prepend-icon="edit"
persistent-hint
@change="onChange"
>
</v-checkbox>
</v-flex>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
v-model="settings.features.delete"
:disabled="busy"
class="ma-0 pa-0 input-delete"
color="secondary-dark"
:label="$gettext('Delete')"
:hint="$gettext('Permanently remove files to free up storage.')"
prepend-icon="delete"
persistent-hint
@change="onChange"
>
</v-checkbox>
</v-flex>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
v-model="settings.features.import"
:disabled="busy || config.readonly || isDemo"
class="ma-0 pa-0 input-import"
color="secondary-dark"
:label="$gettext('Import')"
:hint="$gettext('Imported files will be sorted by date and given a unique name.')"
prepend-icon="create_new_folder"
persistent-hint
@change="onChange"
>
</v-checkbox>
</v-flex>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
v-model="settings.features.share"
:disabled="busy"
class="ma-0 pa-0 input-share"
color="secondary-dark"
:label="$gettext('Share')"
:hint="$gettext('Upload to WebDAV and share links with friends.')"
prepend-icon="share"
persistent-hint
@change="onChange"
>
</v-checkbox>
</v-flex>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
v-model="settings.features.private"
:disabled="busy"
class="ma-0 pa-0 input-private"
color="secondary-dark"
:label="$gettext('Private')"
:hint="$gettext('Exclude content marked as private from search results, shared albums, labels, and places.')"
prepend-icon="lock"
persistent-hint
@change="onChange"
>
</v-checkbox>
</v-flex>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
v-model="settings.features.archive"
:disabled="busy"
class="ma-0 pa-0 input-archive"
color="secondary-dark"
:label="$gettext('Archive')"
:hint="$gettext('Hide photos that have been moved to archive.')"
prepend-icon="archive"
persistent-hint
@change="onChange"
>
</v-checkbox>
</v-flex>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
v-model="settings.features.files"
:disabled="busy"
class="ma-0 pa-0 input-files"
color="secondary-dark"
:label="$gettext('Originals')"
:hint="$gettext('Browse indexed files and folders in Library.')"
prepend-icon="snippet_folder"
persistent-hint
@change="onChange"
>
</v-checkbox>
</v-flex>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
v-model="settings.features.people"
@@ -226,6 +91,141 @@
</v-checkbox>
</v-flex>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
v-model="settings.features.private"
:disabled="busy"
class="ma-0 pa-0 input-private"
color="secondary-dark"
:label="$gettext('Private')"
:hint="$gettext('Exclude content marked as private from search results, shared albums, labels, and places.')"
prepend-icon="lock"
persistent-hint
@change="onChange"
>
</v-checkbox>
</v-flex>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
v-model="settings.features.upload"
:disabled="busy || config.readonly || isDemo"
class="ma-0 pa-0 input-upload"
color="secondary-dark"
:label="$gettext('Upload')"
:hint="$gettext('Add files to your library via Web Upload.')"
prepend-icon="cloud_upload"
persistent-hint
@change="onChange"
>
</v-checkbox>
</v-flex>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
v-model="settings.features.download"
:disabled="busy || isDemo"
class="ma-0 pa-0 input-download"
color="secondary-dark"
:label="$gettext('Download')"
:hint="$gettext('Download single files and zip archives.')"
prepend-icon="get_app"
persistent-hint
@change="onChange"
>
</v-checkbox>
</v-flex>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
v-model="settings.features.import"
:disabled="busy || config.readonly || isDemo"
class="ma-0 pa-0 input-import"
color="secondary-dark"
:label="$gettext('Import')"
:hint="$gettext('Imported files will be sorted by date and given a unique name.')"
prepend-icon="create_new_folder"
persistent-hint
@change="onChange"
>
</v-checkbox>
</v-flex>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
v-model="settings.features.share"
:disabled="busy"
class="ma-0 pa-0 input-share"
color="secondary-dark"
:label="$gettext('Share')"
:hint="$gettext('Upload to WebDAV and share links with friends.')"
prepend-icon="share"
persistent-hint
@change="onChange"
>
</v-checkbox>
</v-flex>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
v-model="settings.features.edit"
:disabled="busy || isDemo"
class="ma-0 pa-0 input-edit"
color="secondary-dark"
:label="$gettext('Edit')"
:hint="$gettext('Change photo titles, locations, and other metadata.')"
prepend-icon="edit"
persistent-hint
@change="onChange"
>
</v-checkbox>
</v-flex>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
v-model="settings.features.archive"
:disabled="busy"
class="ma-0 pa-0 input-archive"
color="secondary-dark"
:label="$gettext('Archive')"
:hint="$gettext('Hide photos that have been moved to archive.')"
prepend-icon="archive"
persistent-hint
@change="onChange"
>
</v-checkbox>
</v-flex>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
v-model="settings.features.delete"
:disabled="busy"
class="ma-0 pa-0 input-delete"
color="secondary-dark"
:label="$gettext('Delete')"
:hint="$gettext('Permanently remove files to free up storage.')"
prepend-icon="delete"
persistent-hint
@change="onChange"
>
</v-checkbox>
</v-flex>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
v-model="settings.features.services"
:disabled="busy"
class="ma-0 pa-0 input-services"
color="secondary-dark"
:label="$gettext('Services')"
:hint="$gettext('Share your pictures with other apps and services.')"
prepend-icon="sync_alt"
persistent-hint
@change="onChange"
>
</v-checkbox>
</v-flex>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
v-model="settings.features.library"
@@ -233,7 +233,7 @@
class="ma-0 pa-0 input-library"
color="secondary-dark"
:label="$gettext('Library')"
:hint="$gettext('Show Library in navigation menu.')"
:hint="$gettext('Index and import files through the user interface.')"
prepend-icon="camera_roll"
persistent-hint
@change="onChange"
@@ -241,6 +241,21 @@
</v-checkbox>
</v-flex>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
v-model="settings.features.files"
:disabled="busy"
class="ma-0 pa-0 input-files"
color="secondary-dark"
:label="$gettext('Originals')"
:hint="$gettext('Browse indexed files and folders in Library.')"
prepend-icon="snippet_folder"
persistent-hint
@change="onChange"
>
</v-checkbox>
</v-flex>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
v-model="settings.features.logs"
@@ -249,7 +264,22 @@
color="secondary-dark"
:label="$gettext('Logs')"
:hint="$gettext('Show server logs in Library.')"
prepend-icon="notes"
prepend-icon="grading"
persistent-hint
@change="onChange"
>
</v-checkbox>
</v-flex>
<v-flex xs12 sm6 lg3 class="px-2 pb-2 pt-2">
<v-checkbox
v-model="settings.features.account"
:disabled="busy || isDemo"
class="ma-0 pa-0 input-places"
color="secondary-dark"
:label="$gettext('Account')"
:hint="$gettext('Change personal profile and security settings.')"
prepend-icon="admin_panel_settings"
persistent-hint
@change="onChange"
>

View File

@@ -91,7 +91,7 @@
<script>
import Settings from "model/settings";
import Account from "model/account";
import Service from "model/service";
import {DateTime} from "luxon";
export default {
@@ -150,7 +150,7 @@ export default {
return DateTime.fromISO(time).toLocaleString(DateTime.DATE_FULL);
},
load() {
Account.search({count: 2000}).then(r => this.results = r.models);
Service.search({count: 2000}).then(r => this.results = r.models);
},
remove(model) {
this.model = model.clone();

View File

@@ -132,7 +132,7 @@ describe("common/session", () => {
};
session.setData(values2);
const result2 = session.getDisplayName();
assert.equal(result2, "Bar");
assert.equal(result2, "Admin");
session.deleteData();
});

View File

@@ -140,8 +140,8 @@ Mock.onPost("api/v1/session").reply(200, { id: "123", data: { token: "123token"
Mock.onGet("api/v1/settings").reply(200, { download: true, language: "de" }, mockHeaders);
Mock.onPost("api/v1/settings").reply(200, { download: true, language: "en" }, mockHeaders);
Mock.onGet("api/v1/accounts/123/folders").reply(200, { foo: "folders" }, mockHeaders);
Mock.onPost("api/v1/accounts/123/share").reply(200, { foo: "share" }, mockHeaders);
Mock.onGet("api/v1/services/123/folders").reply(200, { foo: "folders" }, mockHeaders);
Mock.onPost("api/v1/services/123/upload").reply(200, { foo: "upload" }, mockHeaders);
Mock.onGet("api/v1/folders/2011/10-Halloween", {
params: { recursive: true, uncached: true },

View File

@@ -1,39 +1,39 @@
import "../fixtures";
import Account from "model/account";
import Service from "model/service";
import Photo from "model/photo";
let chai = require("chai/chai");
let assert = chai.assert;
describe("model/account", () => {
it("should get account defaults", () => {
describe("model/service", () => {
it("should get service defaults", () => {
const values = { ID: 5 };
const account = new Account(values);
const result = account.getDefaults();
const service = new Service(values);
const result = service.getDefaults();
assert.equal(result.ID, 0);
assert.equal(result.AccShare, true);
assert.equal(result.AccName, "");
});
it("should get account entity name", () => {
it("should get service entity name", () => {
const values = { ID: 5, AccName: "Test Name" };
const account = new Account(values);
const result = account.getEntityName();
const service = new Service(values);
const result = service.getEntityName();
assert.equal(result, "Test Name");
});
it("should get account id", () => {
it("should get service id", () => {
const values = { ID: 5, AccName: "Test Name" };
const account = new Account(values);
const result = account.getId();
const service = new Service(values);
const result = service.getId();
assert.equal(result, 5);
});
it("should get folders", (done) => {
const values = { ID: 123, AccName: "Test Name" };
const account = new Account(values);
account
const service = new Service(values);
service
.Folders()
.then((response) => {
assert.equal(response.foo, "folders");
@@ -46,16 +46,16 @@ describe("model/account", () => {
it("should get share photos", (done) => {
const values = { ID: 123, AccName: "Test Name" };
const account = new Account(values);
const service = new Service(values);
const values1 = { ID: 5, Title: "Crazy Cat", UID: 789 };
const photo = new Photo(values1);
const values2 = { ID: 6, Title: "Crazy Cat 2", UID: 783 };
const photo2 = new Photo(values2);
const Photos = [photo, photo2];
account
.Share(Photos, "destination")
service
.Upload(Photos, "destination")
.then((response) => {
assert.equal(response.foo, "share");
assert.equal(response.foo, "upload");
done();
})
.catch((error) => {
@@ -64,12 +64,12 @@ describe("model/account", () => {
});
it("should get collection resource", () => {
const result = Account.getCollectionResource();
assert.equal(result, "accounts");
const result = Service.getCollectionResource();
assert.equal(result, "services");
});
it("should get model name", () => {
const result = Account.getModelName();
const result = Service.getModelName();
assert.equal(result, "Account");
});
});

View File

@@ -58,7 +58,7 @@ var Resources = ACL{
ResourceShares: Roles{
RoleAdmin: GrantFullAccess,
},
ResourceAccounts: Roles{
ResourceServices: Roles{
RoleAdmin: GrantFullAccess,
},
ResourceUsers: Roles{

View File

@@ -18,7 +18,7 @@ const (
ResourceSettings Resource = "settings"
ResourcePassword Resource = "password"
ResourceUsers Resource = "users"
ResourceAccounts Resource = "accounts"
ResourceServices Resource = "services"
ResourceFiles Resource = "files"
ResourceFolders Resource = "folders"
ResourceShares Resource = "shares"

View File

@@ -11,10 +11,10 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/search"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/clean"
)
@@ -22,7 +22,7 @@ var albumMutex = sync.Mutex{}
// SaveAlbumAsYaml saves album data as YAML file.
func SaveAlbumAsYaml(a entity.Album) {
c := service.Config()
c := get.Config()
// Write YAML sidecar file (optional).
if !c.BackupYaml() {

View File

@@ -9,8 +9,8 @@ import (
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/search"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -36,7 +36,7 @@ func SearchAlbums(router *gin.RouterGroup) {
return
}
conf := service.Config()
conf := get.Config()
// Ignore private flag if feature is disabled.
if !conf.Settings().Features.Private {

View File

@@ -6,12 +6,12 @@ import (
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/get"
)
// UpdateClientConfig publishes updated client configuration values over the websocket connections.
func UpdateClientConfig() {
event.Publish("config.updated", event.Data{"config": service.Config().ClientUser(false)})
event.Publish("config.updated", event.Data{"config": get.Config().ClientUser(false)})
}
// GetClientConfig returns the client configuration values as JSON.
@@ -20,7 +20,7 @@ func UpdateClientConfig() {
func GetClientConfig(router *gin.RouterGroup) {
router.GET("/config", func(c *gin.Context) {
s := Session(SessionID(c))
conf := service.Config()
conf := get.Config()
if s == nil {
c.JSON(http.StatusOK, conf.ClientPublic())

View File

@@ -12,7 +12,7 @@ import (
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/get"
)
func TestMain(m *testing.M) {
@@ -21,7 +21,7 @@ func TestMain(m *testing.M) {
event.AuditLog = log
c := config.TestConfig()
service.SetConfig(c)
get.SetConfig(c)
code := m.Run()
@@ -37,7 +37,7 @@ func NewApiTest() (app *gin.Engine, router *gin.RouterGroup, conf *config.Config
app = gin.New()
router = app.Group("/api/v1")
return app, router, service.Config()
return app, router, get.Config()
}
// Executes an API request with an empty request body.

View File

@@ -14,7 +14,7 @@ import (
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/pkg/rnd"
)
@@ -57,7 +57,7 @@ func WebSocket(router *gin.RouterGroup) {
return
}
conf := service.Config()
conf := get.Config()
if conf == nil {
return

View File

@@ -9,9 +9,9 @@ import (
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/server/limiter"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/clean"
)
@@ -20,7 +20,7 @@ import (
// PUT /api/v1/users/:uid/password
func ChangePassword(router *gin.RouterGroup) {
router.PUT("/users/:uid/password", func(c *gin.Context) {
conf := service.Config()
conf := get.Config()
// You cannot change any passwords without authentication and settings enabled.
if conf.Public() || conf.DisableSettings() {

View File

@@ -4,7 +4,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/session"
"github.com/photoprism/photoprism/pkg/clean"
)
@@ -23,14 +23,14 @@ func SessionID(c *gin.Context) (sessId string) {
// Session finds the client session for the given ID or returns nil otherwise.
func Session(id string) *entity.Session {
// Return default session when public mode is enabled.
if service.Config().Public() {
return service.Session().Public()
if get.Config().Public() {
return get.Session().Public()
} else if id == "" {
return nil
}
// Find session or otherwise return nil.
s, err := service.Session().Get(id)
s, err := get.Session().Get(id)
if err != nil {
return nil

View File

@@ -8,9 +8,9 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/server/limiter"
"github.com/photoprism/photoprism/internal/service"
)
// CreateSession creates a new client session and returns it as JSON if authentication was successful.
@@ -26,11 +26,11 @@ func CreateSession(router *gin.RouterGroup) {
return
}
conf := service.Config()
conf := get.Config()
// Skip authentication if app is running in public mode.
if conf.Public() {
sess := service.Session().Public()
sess := get.Session().Public()
data := gin.H{
"status": "ok",
"id": sess.ID,
@@ -57,7 +57,7 @@ func CreateSession(router *gin.RouterGroup) {
sess = s
} else {
// Create new session.
sess = service.Session().New(c)
sess = get.Session().New(c)
isNew = true
}
@@ -65,7 +65,7 @@ func CreateSession(router *gin.RouterGroup) {
if err := sess.LogIn(f, c); err != nil {
c.AbortWithStatusJSON(sess.HttpStatus(), gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
return
} else if sess, err = service.Session().Save(sess); err != nil {
} else if sess, err = get.Session().Save(sess); err != nil {
event.AuditErr([]string{ClientIP(c), "%s"}, err)
c.AbortWithStatusJSON(sess.HttpStatus(), gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
return

View File

@@ -6,7 +6,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/pkg/clean"
)
@@ -20,12 +20,12 @@ func DeleteSession(router *gin.RouterGroup) {
if id == "" {
AbortBadRequest(c)
return
} else if service.Config().Public() {
} else if get.Config().Public() {
c.JSON(http.StatusOK, gin.H{"status": "authentication disabled", "id": id})
return
}
if err := service.Session().Delete(id); err != nil {
if err := get.Session().Delete(id); err != nil {
event.AuditErr([]string{ClientIP(c), "session %s"}, err)
} else {
event.AuditDebug([]string{ClientIP(c), "session deleted"})

View File

@@ -5,7 +5,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/pkg/clean"
)
@@ -50,7 +50,7 @@ func GetSession(router *gin.RouterGroup) {
"id": sess.ID,
"user": sess.User(),
"data": sess.Data(),
"config": service.Config().ClientSession(sess),
"config": get.Config().ClientSession(sess),
}
c.JSON(http.StatusOK, data)

View File

@@ -8,8 +8,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/clean"
)
@@ -18,7 +18,7 @@ import (
// GET /s/:token/...
func Shares(router *gin.RouterGroup) {
router.GET("/:token", func(c *gin.Context) {
conf := service.Config()
conf := get.Config()
token := clean.Token(c.Param("token"))
links := entity.FindValidLinks(token, "")
@@ -37,7 +37,7 @@ func Shares(router *gin.RouterGroup) {
})
router.GET("/:token/:shared", func(c *gin.Context) {
conf := service.Config()
conf := get.Config()
token := clean.Token(c.Param("token"))
shared := clean.Token(c.Param("shared"))

View File

@@ -13,10 +13,10 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/clean"
)
@@ -45,7 +45,7 @@ func BatchPhotosArchive(router *gin.RouterGroup) {
log.Infof("photos: archiving %s", clean.Log(f.String()))
if service.Config().BackupYaml() {
if get.Config().BackupYaml() {
// Fetch selection from index.
photos, err := query.SelectedPhotos(f)
@@ -108,7 +108,7 @@ func BatchPhotosRestore(router *gin.RouterGroup) {
log.Infof("photos: restoring %s", clean.Log(f.String()))
if service.Config().BackupYaml() {
if get.Config().BackupYaml() {
// Fetch selection from index.
photos, err := query.SelectedPhotos(f)
@@ -346,7 +346,7 @@ func BatchPhotosDelete(router *gin.RouterGroup) {
return
}
conf := service.Config()
conf := get.Config()
if conf.ReadOnly() || !conf.Settings().Features.Delete {
AbortFeatureDisabled(c)

View File

@@ -6,7 +6,7 @@ import (
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/thumb"
)
@@ -40,7 +40,7 @@ func CacheKey(ns, uid, name string) string {
// RemoveFromFolderCache removes an item from the folder cache e.g. after indexing.
func RemoveFromFolderCache(rootName string) {
cache := service.FolderCache()
cache := get.FolderCache()
cacheKey := fmt.Sprintf("folder:%s:%t:%t", rootName, true, false)
@@ -55,7 +55,7 @@ func RemoveFromFolderCache(rootName string) {
// RemoveFromAlbumCoverCache removes covers by album UID e.g. after adding or removing photos.
func RemoveFromAlbumCoverCache(uid string) {
cache := service.CoverCache()
cache := get.CoverCache()
for thumbName := range thumb.Sizes {
cacheKey := CacheKey(albumCover, uid, string(thumbName))
@@ -72,7 +72,7 @@ func RemoveFromAlbumCoverCache(uid string) {
// FlushCoverCache clears the complete cover cache.
func FlushCoverCache() {
service.CoverCache().Flush()
get.CoverCache().Flush()
if err := query.UpdateCovers(); err != nil {
log.Error(err)

View File

@@ -10,8 +10,8 @@ import (
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
)
@@ -22,7 +22,7 @@ import (
func GetConfigOptions(router *gin.RouterGroup) {
router.GET("/config/options", func(c *gin.Context) {
s := Auth(c, acl.ResourceConfig, acl.AccessAll)
conf := service.Config()
conf := get.Config()
// Abort if permission was not granted.
if s.Invalid() || conf.Public() || conf.DisableSettings() {
@@ -40,7 +40,7 @@ func GetConfigOptions(router *gin.RouterGroup) {
func SaveConfigOptions(router *gin.RouterGroup) {
router.POST("/config/options", func(c *gin.Context) {
s := Auth(c, acl.ResourceConfig, acl.ActionManage)
conf := service.Config()
conf := get.Config()
if s.Invalid() || conf.Public() || conf.DisableSettings() {
AbortForbidden(c)

View File

@@ -8,8 +8,8 @@ import (
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/customize"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/service"
)
// GetSettings returns the user app settings as JSON.
@@ -24,7 +24,7 @@ func GetSettings(router *gin.RouterGroup) {
return
}
settings := service.Config().SessionSettings(s)
settings := get.Config().SessionSettings(s)
if settings == nil {
Abort(c, http.StatusNotFound, i18n.ErrNotFound)
@@ -47,7 +47,7 @@ func SaveSettings(router *gin.RouterGroup) {
return
}
conf := service.Config()
conf := get.Config()
if conf.DisableSettings() {
AbortForbidden(c)
@@ -101,6 +101,6 @@ func SaveSettings(router *gin.RouterGroup) {
event.InfoMsg(i18n.MsgSettingsSaved)
c.JSON(http.StatusOK, service.Config().SessionSettings(s))
c.JSON(http.StatusOK, get.Config().SessionSettings(s))
})
}

View File

@@ -7,8 +7,8 @@ import (
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/clean"
)
@@ -39,7 +39,7 @@ func Connect(router *gin.RouterGroup) {
return
}
conf := service.Config()
conf := get.Config()
if conf.Public() {
Abort(c, http.StatusForbidden, i18n.ErrPublic)

View File

@@ -7,9 +7,9 @@ import (
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
@@ -38,7 +38,7 @@ func AlbumCover(router *gin.RouterGroup) {
}
start := time.Now()
conf := service.Config()
conf := get.Config()
thumbName := thumb.Name(clean.Token(c.Param("size")))
uid := clean.UID(c.Param("uid"))
@@ -50,7 +50,7 @@ func AlbumCover(router *gin.RouterGroup) {
return
}
cache := service.CoverCache()
cache := get.CoverCache()
cacheKey := CacheKey(albumCover, uid, string(thumbName))
if cacheData, ok := cache.Get(cacheKey); ok {
@@ -151,7 +151,7 @@ func LabelCover(router *gin.RouterGroup) {
}
start := time.Now()
conf := service.Config()
conf := get.Config()
thumbName := thumb.Name(clean.Token(c.Param("size")))
uid := clean.UID(c.Param("uid"))
@@ -163,7 +163,7 @@ func LabelCover(router *gin.RouterGroup) {
return
}
cache := service.CoverCache()
cache := get.CoverCache()
cacheKey := CacheKey(labelCover, uid, string(thumbName))
if cacheData, ok := cache.Get(cacheKey); ok {

View File

@@ -8,11 +8,11 @@ import (
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/search"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
@@ -28,7 +28,7 @@ func DownloadAlbum(router *gin.RouterGroup) {
return
}
conf := service.Config()
conf := get.Config()
if !conf.Settings().Features.Download {
AbortFeatureDisabled(c)

View File

@@ -7,9 +7,9 @@ import (
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
@@ -29,7 +29,7 @@ func DownloadName(c *gin.Context) customize.DownloadName {
case "original":
return customize.DownloadNameOriginal
default:
return service.Config().Settings().Download.Name
return get.Config().Settings().Download.Name
}
}

View File

@@ -6,9 +6,9 @@ import (
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -46,7 +46,7 @@ func GetErrors(router *gin.RouterGroup) {
// DELETE /api/v1/errors
func DeleteErrors(router *gin.RouterGroup) {
router.DELETE("/errors", func(c *gin.Context) {
conf := service.Config()
conf := get.Config()
// Disabled in public mode so that attackers cannot cover their tracks.
if conf.Public() {

View File

@@ -7,8 +7,8 @@ import (
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/service"
)
// SendFeedback sends a feedback message.
@@ -16,7 +16,7 @@ import (
// POST /api/v1/feedback
func SendFeedback(router *gin.RouterGroup) {
router.POST("/feedback", func(c *gin.Context) {
conf := service.Config()
conf := get.Config()
if conf.Public() {
Abort(c, http.StatusForbidden, i18n.ErrPublic)

View File

@@ -9,10 +9,10 @@ import (
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
)
// DeleteFile removes a file from storage.
@@ -30,7 +30,7 @@ func DeleteFile(router *gin.RouterGroup) {
return
}
conf := service.Config()
conf := get.Config()
if conf.ReadOnly() || !conf.Settings().Features.Edit {
Abort(c, http.StatusForbidden, i18n.ErrReadOnly)

View File

@@ -7,9 +7,9 @@ import (
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
@@ -36,7 +36,7 @@ func FolderCover(router *gin.RouterGroup) {
}
start := time.Now()
conf := service.Config()
conf := get.Config()
uid := c.Param("uid")
thumbName := thumb.Name(clean.Token(c.Param("size")))
download := c.Query("download") != ""
@@ -59,7 +59,7 @@ func FolderCover(router *gin.RouterGroup) {
}
}
cache := service.CoverCache()
cache := get.CoverCache()
cacheKey := CacheKey(folderCover, uid, string(thumbName))
if cacheData, ok := cache.Get(cacheKey); ok {

View File

@@ -12,8 +12,8 @@ import (
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/clean"
)
@@ -30,7 +30,7 @@ type FoldersResponse struct {
//
// GET /api/v1/folders/originals
func SearchFoldersOriginals(router *gin.RouterGroup) {
conf := service.Config()
conf := get.Config()
SearchFolders(router, "originals", entity.RootOriginals, conf.OriginalsPath())
}
@@ -38,7 +38,7 @@ func SearchFoldersOriginals(router *gin.RouterGroup) {
//
// GET /api/v1/folders/import
func SearchFoldersImport(router *gin.RouterGroup) {
conf := service.Config()
conf := get.Config()
SearchFolders(router, "import", entity.RootImport, conf.ImportPath())
}
@@ -62,7 +62,7 @@ func SearchFolders(router *gin.RouterGroup, urlPath, rootName, rootPath string)
return
}
cache := service.FolderCache()
cache := get.FolderCache()
recursive := f.Recursive
listFiles := f.Files
uncached := listFiles || f.Uncached

View File

@@ -15,10 +15,10 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
)
@@ -38,7 +38,7 @@ func StartImport(router *gin.RouterGroup) {
return
}
conf := service.Config()
conf := get.Config()
if conf.ReadOnly() || !conf.Settings().Features.Import {
AbortFeatureDisabled(c)
@@ -76,7 +76,7 @@ func StartImport(router *gin.RouterGroup) {
importPath = path.Join(importPath, srcFolder)
imp := service.Import()
imp := get.Import()
RemoveFromFolderCache(entity.RootImport)
@@ -125,7 +125,7 @@ func StartImport(router *gin.RouterGroup) {
log.Infof("import: no new files found to import", clean.Log(importPath))
} else {
log.Infof("import: imported %s", english.Plural(n, "file", "files"))
if moments := service.Moments(); moments == nil {
if moments := get.Moments(); moments == nil {
log.Warnf("import: moments service not set - possible bug")
} else if err := moments.Start(); err != nil {
log.Warnf("moments: %s", err)
@@ -168,14 +168,14 @@ func CancelImport(router *gin.RouterGroup) {
return
}
conf := service.Config()
conf := get.Config()
if conf.ReadOnly() || !conf.Settings().Features.Import {
AbortFeatureDisabled(c)
return
}
imp := service.Import()
imp := get.Import()
imp.Cancel()

View File

@@ -11,9 +11,9 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -29,7 +29,7 @@ func StartIndexing(router *gin.RouterGroup) {
return
}
conf := service.Config()
conf := get.Config()
settings := conf.Settings()
if !settings.Features.Library {
@@ -60,7 +60,7 @@ func StartIndexing(router *gin.RouterGroup) {
}
// Start indexing.
ind := service.Index()
ind := get.Index()
indexed := ind.Start(indOpt)
RemoveFromFolderCache(entity.RootOriginals)
@@ -72,7 +72,7 @@ func StartIndexing(router *gin.RouterGroup) {
}
// Start purging.
prg := service.Purge()
prg := get.Purge()
if files, photos, err := prg.Start(prgOpt); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UpperFirst(err.Error())})
@@ -85,7 +85,7 @@ func StartIndexing(router *gin.RouterGroup) {
"step": "moments",
})
moments := service.Moments()
moments := get.Moments()
if err := moments.Start(); err != nil {
log.Warnf("moments: %s", err)
@@ -115,14 +115,14 @@ func CancelIndexing(router *gin.RouterGroup) {
return
}
conf := service.Config()
conf := get.Config()
if !conf.Settings().Features.Library {
AbortFeatureDisabled(c)
return
}
ind := service.Index()
ind := get.Index()
ind.Cancel()

View File

@@ -12,15 +12,15 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
)
// Checks if background worker runs less than once per hour.
func wakeupIntervalTooHigh(c *gin.Context) bool {
if conf := service.Config(); conf.Unsafe() {
if conf := get.Config(); conf.Unsafe() {
return false
} else if i := conf.WakeupInterval(); i > time.Hour {
Abort(c, http.StatusForbidden, i18n.ErrWakeupInterval, i.String())
@@ -41,7 +41,7 @@ func findFileMarker(c *gin.Context) (file *entity.File, marker *entity.Marker, e
}
// Check feature flags.
conf := service.Config()
conf := get.Config()
if !conf.Settings().Features.People {
AbortFeatureDisabled(c)
return nil, nil, fmt.Errorf("feature disabled")
@@ -119,7 +119,7 @@ func UpdateMarker(router *gin.RouterGroup) {
return
} else if changed {
if marker.FaceID != "" && marker.SubjUID != "" && marker.SubjSrc == entity.SrcManual {
if res, err := service.Faces().Optimize(); err != nil {
if res, err := get.Faces().Optimize(); err != nil {
log.Errorf("faces: %s (optimize)", err)
} else if res.Merged > 0 {
log.Infof("faces: merged %s", english.Plural(res.Merged, "cluster", "clusters"))

View File

@@ -6,8 +6,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -22,7 +22,7 @@ func GetMomentsTime(router *gin.RouterGroup) {
return
}
conf := service.Config()
conf := get.Config()
result, err := query.MomentsTime(1, conf.Settings().Features.Private)

View File

@@ -10,10 +10,10 @@ import (
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/clean"
)
@@ -33,7 +33,7 @@ func PhotoUnstack(router *gin.RouterGroup) {
return
}
conf := service.Config()
conf := get.Config()
fileUid := clean.UID(c.Param("file_uid"))
file, err := query.FileByUID(fileUid)
@@ -170,7 +170,7 @@ func PhotoUnstack(router *gin.RouterGroup) {
}
}
ind := service.Index()
ind := get.Index()
// Index unstacked files.
if res := ind.FileName(unstackFile.FileName(), photoprism.IndexOptionsSingle()); res.Failed() {

View File

@@ -10,17 +10,17 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
)
// SavePhotoAsYaml saves photo data as YAML file.
func SavePhotoAsYaml(p entity.Photo) {
c := service.Config()
c := get.Config()
// Write YAML sidecar file (optional).
if !c.BackupYaml() {

View File

@@ -10,9 +10,9 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/search"
"github.com/photoprism/photoprism/internal/service"
)
// SearchPhotos searches the pictures index and returns the result as JSON.
@@ -36,7 +36,7 @@ func SearchPhotos(router *gin.RouterGroup) {
return f, s, err
}
settings := service.Config().Settings()
settings := get.Config().Settings()
// Ignore private flag if feature is disabled.
if !settings.Features.Private {
@@ -91,7 +91,7 @@ func SearchPhotos(router *gin.RouterGroup) {
return
}
conf := service.Config()
conf := get.Config()
result, count, err := search.UserPhotosViewerResults(f, s, conf.ContentUri(), conf.ApiUri(), s.PreviewToken, s.DownloadToken)

View File

@@ -9,8 +9,8 @@ import (
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/search"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -38,7 +38,7 @@ func SearchGeo(router *gin.RouterGroup) {
return
}
conf := service.Config()
conf := get.Config()
settings := conf.Settings()
// Ignore private flag if feature is disabled.

View File

@@ -6,8 +6,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/react"
)
@@ -31,7 +31,7 @@ func LikePhoto(router *gin.RouterGroup) {
return
}
if service.Config().Experimental() && acl.Resources.Allow(acl.ResourcePhotos, s.User().AclRole(), acl.ActionReact) {
if get.Config().Experimental() && acl.Resources.Allow(acl.ResourcePhotos, s.User().AclRole(), acl.ActionReact) {
logWarn("react", m.React(s.User(), react.Find("love")))
}
@@ -71,7 +71,7 @@ func DislikePhoto(router *gin.RouterGroup) {
return
}
if service.Config().Experimental() && acl.Resources.Allow(acl.ResourcePhotos, s.User().AclRole(), acl.ActionReact) {
if get.Config().Experimental() && acl.Resources.Allow(acl.ResourcePhotos, s.User().AclRole(), acl.ActionReact) {
logWarn("react", m.UnReact(s.User()))
}

View File

@@ -3,8 +3,6 @@ package api
import (
"fmt"
"net/http"
"path"
"strings"
"time"
"github.com/gin-gonic/gin"
@@ -13,9 +11,9 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/workers"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
@@ -23,25 +21,21 @@ import (
// Namespaces for caching and logs.
const (
accountFolder = "account-folder"
serviceFolder = "service-folder"
)
// GetAccount returns an account as JSON.
// GetService returns an account as JSON.
//
// GET /api/v1/accounts/:id
//
// Parameters:
//
// id: string Account ID as returned by the API
func GetAccount(router *gin.RouterGroup) {
router.GET("/accounts/:id", func(c *gin.Context) {
s := Auth(c, acl.ResourceAccounts, acl.ActionView)
// GET /api/v1/services/:id
func GetService(router *gin.RouterGroup) {
router.GET("/services/:id", func(c *gin.Context) {
s := Auth(c, acl.ResourceServices, acl.ActionView)
if s.Abort(c) {
return
}
conf := service.Config()
conf := get.Config()
if conf.Demo() || conf.DisableSettings() {
AbortForbidden(c)
@@ -58,22 +52,18 @@ func GetAccount(router *gin.RouterGroup) {
})
}
// GetAccountFolders returns folders that belong to an account as JSON.
// GetServiceFolders returns folders that belong to an account as JSON.
//
// GET /api/v1/accounts/:id/folders
//
// Parameters:
//
// id: string Account ID as returned by the API
func GetAccountFolders(router *gin.RouterGroup) {
router.GET("/accounts/:id/folders", func(c *gin.Context) {
s := Auth(c, acl.ResourceAccounts, acl.ActionView)
// GET /api/v1/services/:id/folders
func GetServiceFolders(router *gin.RouterGroup) {
router.GET("/services/:id/folders", func(c *gin.Context) {
s := Auth(c, acl.ResourceServices, acl.ActionView)
if s.Abort(c) {
return
}
conf := service.Config()
conf := get.Config()
if conf.Demo() || conf.DisableSettings() {
AbortForbidden(c)
@@ -82,8 +72,8 @@ func GetAccountFolders(router *gin.RouterGroup) {
start := time.Now()
id := clean.IdUint(c.Param("id"))
cache := service.FolderCache()
cacheKey := fmt.Sprintf("%s:%d", accountFolder, id)
cache := get.FolderCache()
cacheKey := fmt.Sprintf("%s:%d", serviceFolder, id)
if cacheData, ok := cache.Get(cacheKey); ok {
cached := cacheData.(fs.FileInfos)
@@ -104,7 +94,7 @@ func GetAccountFolders(router *gin.RouterGroup) {
list, err := m.Directories()
if err != nil {
log.Errorf("%s: %s", accountFolder, err.Error())
log.Errorf("%s: %s", serviceFolder, err.Error())
Abort(c, http.StatusBadRequest, i18n.ErrConnectionFailed)
return
}
@@ -116,101 +106,38 @@ func GetAccountFolders(router *gin.RouterGroup) {
})
}
// ShareWithAccount uploads files to the selected account.
// AddService creates a new remote account configuration.
//
// GET /api/v1/accounts/:id/share
//
// Parameters:
//
// id: string Account ID as returned by the API
func ShareWithAccount(router *gin.RouterGroup) {
router.POST("/accounts/:id/share", func(c *gin.Context) {
s := Auth(c, acl.ResourceAccounts, acl.ActionUpload)
// POST /api/v1/services
func AddService(router *gin.RouterGroup) {
router.POST("/services", func(c *gin.Context) {
s := Auth(c, acl.ResourceServices, acl.ActionCreate)
if s.Abort(c) {
return
}
id := clean.IdUint(c.Param("id"))
m, err := query.AccountByID(id)
if err != nil {
Abort(c, http.StatusNotFound, i18n.ErrAccountNotFound)
return
}
var f form.AccountShare
if err := c.BindJSON(&f); err != nil {
AbortBadRequest(c)
return
}
folder := f.Folder
// Find files to share.
selection := query.ShareSelection(m.ShareOriginals())
files, err := query.SelectedFiles(f.Selection, selection)
if err != nil {
AbortEntityNotFound(c)
return
}
var aliases = make(map[string]int)
for _, file := range files {
alias := path.Join(folder, file.ShareBase(0))
key := strings.ToLower(alias)
if seq := aliases[key]; seq > 0 {
alias = file.ShareBase(seq)
}
aliases[key] += 1
entity.FirstOrCreateFileShare(entity.NewFileShare(file.ID, m.ID, alias))
}
workers.RunShare(service.Config())
c.JSON(http.StatusOK, files)
})
}
// CreateAccount creates a new remote account configuration.
//
// POST /api/v1/accounts
func CreateAccount(router *gin.RouterGroup) {
router.POST("/accounts", func(c *gin.Context) {
s := Auth(c, acl.ResourceAccounts, acl.ActionCreate)
if s.Abort(c) {
return
}
conf := service.Config()
conf := get.Config()
if conf.Demo() || conf.DisableSettings() {
AbortForbidden(c)
return
}
var f form.Account
var f form.Service
if err := c.BindJSON(&f); err != nil {
AbortBadRequest(c)
return
}
if err := f.ServiceDiscovery(); err != nil {
if err := f.Discovery(); err != nil {
log.Error(err)
Abort(c, http.StatusBadRequest, i18n.ErrConnectionFailed)
return
}
m, err := entity.CreateAccount(f)
m, err := entity.AddService(f)
if err != nil {
log.Error(err)
@@ -224,22 +151,18 @@ func CreateAccount(router *gin.RouterGroup) {
})
}
// UpdateAccount updates a remote account configuration.
// UpdateService updates a remote account configuration.
//
// PUT /api/v1/accounts/:id
//
// Parameters:
//
// id: string Account ID as returned by the API
func UpdateAccount(router *gin.RouterGroup) {
router.PUT("/accounts/:id", func(c *gin.Context) {
s := Auth(c, acl.ResourceAccounts, acl.ActionUpdate)
// PUT /api/v1/services/:id
func UpdateService(router *gin.RouterGroup) {
router.PUT("/services/:id", func(c *gin.Context) {
s := Auth(c, acl.ResourceServices, acl.ActionUpdate)
if s.Abort(c) {
return
}
conf := service.Config()
conf := get.Config()
if conf.Demo() || conf.DisableSettings() {
AbortForbidden(c)
@@ -256,7 +179,7 @@ func UpdateAccount(router *gin.RouterGroup) {
}
// 1) Init form with model values
f, err := form.NewAccount(m)
f, err := form.NewService(m)
if err != nil {
log.Error(err)
@@ -288,29 +211,25 @@ func UpdateAccount(router *gin.RouterGroup) {
}
if m.AccSync {
workers.RunSync(service.Config())
workers.RunSync(get.Config())
}
c.JSON(http.StatusOK, m)
})
}
// DeleteAccount removes a remote account configuration.
// DeleteService removes a remote account configuration.
//
// DELETE /api/v1/accounts/:id
//
// Parameters:
//
// id: string Account ID as returned by the API
func DeleteAccount(router *gin.RouterGroup) {
router.DELETE("/accounts/:id", func(c *gin.Context) {
s := Auth(c, acl.ResourceAccounts, acl.ActionDelete)
// DELETE /api/v1/services/:id
func DeleteService(router *gin.RouterGroup) {
router.DELETE("/services/:id", func(c *gin.Context) {
s := Auth(c, acl.ResourceServices, acl.ActionDelete)
if s.Abort(c) {
return
}
conf := service.Config()
conf := get.Config()
if conf.Demo() || conf.DisableSettings() {
AbortForbidden(c)

View File

@@ -9,29 +9,29 @@ import (
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/search"
"github.com/photoprism/photoprism/internal/service"
)
// SearchAccounts finds accounts and returns them as JSON.
// SearchServices finds account settings and returns them as JSON.
//
// GET /api/v1/accounts
func SearchAccounts(router *gin.RouterGroup) {
router.GET("/accounts", func(c *gin.Context) {
s := Auth(c, acl.ResourceAccounts, acl.ActionSearch)
// GET /api/v1/services
func SearchServices(router *gin.RouterGroup) {
router.GET("/services", func(c *gin.Context) {
s := Auth(c, acl.ResourceServices, acl.ActionSearch)
if s.Abort(c) {
return
}
conf := service.Config()
conf := get.Config()
if conf.Demo() || conf.DisableSettings() {
c.JSON(http.StatusOK, entity.Accounts{})
c.JSON(http.StatusOK, entity.Services{})
return
}
var f form.SearchAccounts
var f form.SearchServices
err := c.MustBindWith(&f, binding.Form)

View File

@@ -9,12 +9,12 @@ import (
"github.com/stretchr/testify/assert"
)
func TestSearchAccounts(t *testing.T) {
func TestSearchServices(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
app, router, _ := NewApiTest()
SearchAccounts(router)
SearchServices(router)
sess := AuthenticateAdmin(app, router)
r := AuthenticatedRequest(app, "GET", "/api/v1/accounts?count=10", sess)
r := AuthenticatedRequest(app, "GET", "/api/v1/services?count=10", sess)
val := gjson.Get(r.Body.String(), "#(AccName=\"Test Account\").AccURL")
count := gjson.Get(r.Body.String(), "#")
assert.LessOrEqual(t, int64(1), count.Int())
@@ -23,8 +23,8 @@ func TestSearchAccounts(t *testing.T) {
})
t.Run("invalid request", func(t *testing.T) {
app, router, _ := NewApiTest()
SearchAccounts(router)
r := PerformRequest(app, "GET", "/api/v1/accounts?xxx=10")
SearchServices(router)
r := PerformRequest(app, "GET", "/api/v1/services?xxx=10")
assert.Equal(t, http.StatusBadRequest, r.Code)
})
}

View File

@@ -10,88 +10,69 @@ import (
"github.com/photoprism/photoprism/internal/i18n"
)
func TestGetAccount(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
func TestGetService(t *testing.T) {
t.Run("Ok", func(t *testing.T) {
app, router, _ := NewApiTest()
GetAccount(router)
r := PerformRequest(app, "GET", "/api/v1/accounts/1000000")
GetService(router)
r := PerformRequest(app, "GET", "/api/v1/services/1000000")
val := gjson.Get(r.Body.String(), "AccName")
assert.Equal(t, "Test Account", val.String())
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("account not found", func(t *testing.T) {
t.Run("NotFound", func(t *testing.T) {
app, router, _ := NewApiTest()
GetAccount(router)
r := PerformRequest(app, "GET", "/api/v1/accounts/999000")
GetService(router)
r := PerformRequest(app, "GET", "/api/v1/services/999000")
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, i18n.Msg(i18n.ErrAccountNotFound), val.String())
assert.Equal(t, http.StatusNotFound, r.Code)
})
}
func TestGetAccountFolders(t *testing.T) {
t.Run("successful request", func(t *testing.T) {
func TestGetServiceFolders(t *testing.T) {
t.Run("Ok", func(t *testing.T) {
app, router, _ := NewApiTest()
GetAccountFolders(router)
r := PerformRequest(app, "GET", "/api/v1/accounts/1000000/folders")
GetServiceFolders(router)
r := PerformRequest(app, "GET", "/api/v1/services/1000000/folders")
count := gjson.Get(r.Body.String(), "#")
assert.LessOrEqual(t, int64(2), count.Int())
val := gjson.Get(r.Body.String(), "0.abs")
assert.Equal(t, "/Photos", val.String())
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("account not found", func(t *testing.T) {
t.Run("NotFound", func(t *testing.T) {
app, router, _ := NewApiTest()
GetAccountFolders(router)
r := PerformRequest(app, "GET", "/api/v1/accounts/999000/folders")
GetServiceFolders(router)
r := PerformRequest(app, "GET", "/api/v1/services/999000/folders")
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, i18n.Msg(i18n.ErrAccountNotFound), val.String())
assert.Equal(t, http.StatusNotFound, r.Code)
})
}
func TestShareWithAccount(t *testing.T) {
t.Run("invalid request", func(t *testing.T) {
func TestCreateService(t *testing.T) {
t.Run("BadRequest", func(t *testing.T) {
app, router, _ := NewApiTest()
ShareWithAccount(router)
r := PerformRequest(app, "POST", "/api/v1/accounts/1000000/share")
AddService(router)
r := PerformRequest(app, "POST", "/api/v1/services")
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, i18n.Msg(i18n.ErrBadRequest), val.String())
assert.Equal(t, http.StatusBadRequest, r.Code)
})
t.Run("account not found", func(t *testing.T) {
t.Run("ConnectFailed", func(t *testing.T) {
app, router, _ := NewApiTest()
ShareWithAccount(router)
r := PerformRequest(app, "POST", "/api/v1/accounts/999000/share")
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, i18n.Msg(i18n.ErrAccountNotFound), val.String())
assert.Equal(t, http.StatusNotFound, r.Code)
})
}
func TestCreateAccount(t *testing.T) {
t.Run("invalid request", func(t *testing.T) {
app, router, _ := NewApiTest()
CreateAccount(router)
r := PerformRequest(app, "POST", "/api/v1/accounts")
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, i18n.Msg(i18n.ErrBadRequest), val.String())
assert.Equal(t, http.StatusBadRequest, r.Code)
})
t.Run("could not connect", func(t *testing.T) {
app, router, _ := NewApiTest()
CreateAccount(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/accounts", `{"AccName": "CreateTest1", "AccOwner": "Test", "AccUrl": "http://webdav123/", "AccType": "webdav",
AddService(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/services", `{"AccName": "CreateTest1", "AccOwner": "Test", "AccUrl": "http://webdav123/", "AccType": "webdav",
"AccKey": "123", "AccUser": "testuser", "AccPass": "testpasswd", "AccError": "", "AccShare": false, "AccSync": false, "RetryLimit": 3, "SharePath": "", "ShareSize": "", "ShareExpires": 0,
"SyncPath": "", "SyncInterval": 3, "SyncUpload": false, "SyncDownload": false, "SyncFilenames": false, "SyncRaw": false}`)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, i18n.Msg(i18n.ErrConnectionFailed), val.String())
assert.Equal(t, http.StatusBadRequest, r.Code)
})
t.Run("successful request", func(t *testing.T) {
t.Run("Ok", func(t *testing.T) {
app, router, _ := NewApiTest()
CreateAccount(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/accounts", `{"AccName": "CreateTest", "AccOwner": "Test", "AccUrl": "http://dummy-webdav/", "AccType": "webdav",
AddService(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/services", `{"AccName": "CreateTest", "AccOwner": "Test", "AccUrl": "http://dummy-webdav/", "AccType": "webdav",
"AccKey": "123", "AccUser": "admin", "AccPass": "photoprism", "AccError": "", "AccShare": false, "AccSync": false, "RetryLimit": 3, "SharePath": "", "ShareSize": "", "ShareExpires": 0,
"SyncPath": "", "SyncInterval": 3, "SyncUpload": false, "SyncDownload": false, "SyncFilenames": false, "SyncRaw": false}`)
val := gjson.Get(r.Body.String(), "AccOwner")
@@ -100,10 +81,10 @@ func TestCreateAccount(t *testing.T) {
})
}
func TestUpdateAccount(t *testing.T) {
func TestUpdateService(t *testing.T) {
app, router, _ := NewApiTest()
CreateAccount(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/accounts", `{"AccName": "CreateTest3", "AccOwner": "TestUpdate", "AccUrl": "http://dummy-webdav/", "AccType": "webdav",
AddService(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/services", `{"AccName": "CreateTest3", "AccOwner": "TestUpdate", "AccUrl": "http://dummy-webdav/", "AccType": "webdav",
"AccKey": "123", "AccUser": "admin", "AccPass": "photoprism", "AccError": "", "AccShare": false, "AccSync": false, "RetryLimit": 3, "SharePath": "", "ShareSize": "", "ShareExpires": 0,
"SyncPath": "", "SyncInterval": 5, "SyncUpload": false, "SyncDownload": false, "SyncFilenames": false, "SyncRaw": false}`)
val := gjson.Get(r.Body.String(), "AccOwner")
@@ -115,10 +96,10 @@ func TestUpdateAccount(t *testing.T) {
assert.Equal(t, http.StatusOK, r.Code)
id := gjson.Get(r.Body.String(), "ID").String()
t.Run("successful request", func(t *testing.T) {
t.Run("Ok", func(t *testing.T) {
app, router, _ := NewApiTest()
UpdateAccount(router)
r := PerformRequestWithBody(app, "PUT", "/api/v1/accounts/"+id, `{"AccName": "CreateTestUpdated", "AccOwner": "TestUpdated123", "SyncInterval": 9}`)
UpdateService(router)
r := PerformRequestWithBody(app, "PUT", "/api/v1/services/"+id, `{"AccName": "CreateTestUpdated", "AccOwner": "TestUpdated123", "SyncInterval": 9}`)
val := gjson.Get(r.Body.String(), "AccOwner")
assert.Equal(t, "TestUpdated123", val.String())
val2 := gjson.Get(r.Body.String(), "SyncInterval")
@@ -128,52 +109,52 @@ func TestUpdateAccount(t *testing.T) {
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("not found", func(t *testing.T) {
t.Run("NotFound", func(t *testing.T) {
app, router, _ := NewApiTest()
UpdateAccount(router)
r := PerformRequestWithBody(app, "PUT", "/api/v1/accounts/xxx", `{"AccName": "CreateTestUpdated", "AccOwner": "TestUpdated123", "SyncInterval": 9}`)
UpdateService(router)
r := PerformRequestWithBody(app, "PUT", "/api/v1/services/xxx", `{"AccName": "CreateTestUpdated", "AccOwner": "TestUpdated123", "SyncInterval": 9}`)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, i18n.Msg(i18n.ErrAccountNotFound), val.String())
assert.Equal(t, http.StatusNotFound, r.Code)
})
t.Run("changes could not be saved", func(t *testing.T) {
t.Run("SaveFailed", func(t *testing.T) {
app, router, _ := NewApiTest()
UpdateAccount(router)
r := PerformRequestWithBody(app, "PUT", "/api/v1/accounts/"+id, `{"AccName": 6, "AccOwner": "TestUpdated123", "SyncInterval": 9, "AccUrl": "https:xxx.com"}`)
UpdateService(router)
r := PerformRequestWithBody(app, "PUT", "/api/v1/services/"+id, `{"AccName": 6, "AccOwner": "TestUpdated123", "SyncInterval": 9, "AccUrl": "https:xxx.com"}`)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, i18n.Msg(i18n.ErrBadRequest), val.String())
assert.Equal(t, http.StatusBadRequest, r.Code)
})
}
func TestDeleteAccount(t *testing.T) {
func TestDeleteService(t *testing.T) {
app, router, _ := NewApiTest()
CreateAccount(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/accounts", `{"AccName": "DeleteTest", "AccOwner": "TestDelete", "AccUrl": "http://dummy-webdav/", "AccType": "webdav",
AddService(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/services", `{"AccName": "DeleteTest", "AccOwner": "TestDelete", "AccUrl": "http://dummy-webdav/", "AccType": "webdav",
"AccKey": "123", "AccUser": "admin", "AccPass": "photoprism", "AccError": "", "AccShare": false, "AccSync": false, "RetryLimit": 3, "SharePath": "", "ShareSize": "", "ShareExpires": 0,
"SyncPath": "", "SyncInterval": 5, "SyncUpload": false, "SyncDownload": false, "SyncFilenames": false, "SyncRaw": false}`)
assert.Equal(t, http.StatusOK, r.Code)
id := gjson.Get(r.Body.String(), "ID").String()
t.Run("successful request", func(t *testing.T) {
t.Run("Ok", func(t *testing.T) {
app, router, _ := NewApiTest()
DeleteAccount(router)
r := PerformRequest(app, "DELETE", "/api/v1/accounts/"+id)
DeleteService(router)
r := PerformRequest(app, "DELETE", "/api/v1/services/"+id)
val := gjson.Get(r.Body.String(), "AccOwner")
assert.Equal(t, "TestDelete", val.String())
assert.Equal(t, http.StatusOK, r.Code)
GetAccount(router)
r2 := PerformRequest(app, "GET", "/api/v1/accounts/"+id)
GetService(router)
r2 := PerformRequest(app, "GET", "/api/v1/services/"+id)
val2 := gjson.Get(r2.Body.String(), "error")
assert.Equal(t, i18n.Msg(i18n.ErrAccountNotFound), val2.String())
assert.Equal(t, http.StatusNotFound, r2.Code)
})
t.Run("not found", func(t *testing.T) {
t.Run("NotFound", func(t *testing.T) {
app, router, _ := NewApiTest()
DeleteAccount(router)
r := PerformRequest(app, "DELETE", "/api/v1/accounts/xxx")
DeleteService(router)
r := PerformRequest(app, "DELETE", "/api/v1/services/xxx")
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, i18n.Msg(i18n.ErrAccountNotFound), val.String())
assert.Equal(t, http.StatusNotFound, r.Code)

View File

@@ -0,0 +1,77 @@
package api
import (
"net/http"
"path"
"strings"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/workers"
"github.com/photoprism/photoprism/pkg/clean"
)
// UploadToService uploads files to the selected account.
//
// GET /api/v1/services/:id/upload
func UploadToService(router *gin.RouterGroup) {
router.POST("/services/:id/upload", func(c *gin.Context) {
s := Auth(c, acl.ResourceServices, acl.ActionUpload)
if s.Abort(c) {
return
}
id := clean.IdUint(c.Param("id"))
m, err := query.AccountByID(id)
if err != nil {
Abort(c, http.StatusNotFound, i18n.ErrAccountNotFound)
return
}
var f form.SyncUpload
if err := c.BindJSON(&f); err != nil {
AbortBadRequest(c)
return
}
folder := f.Folder
// Find files to share.
selection := query.ShareSelection(m.ShareOriginals())
files, err := query.SelectedFiles(f.Selection, selection)
if err != nil {
AbortEntityNotFound(c)
return
}
var aliases = make(map[string]int)
for _, file := range files {
alias := path.Join(folder, file.ShareBase(0))
key := strings.ToLower(alias)
if seq := aliases[key]; seq > 0 {
alias = file.ShareBase(seq)
}
aliases[key] += 1
entity.FirstOrCreateFileShare(entity.NewFileShare(file.ID, m.ID, alias))
}
workers.RunShare(get.Config())
c.JSON(http.StatusOK, files)
})
}

View File

@@ -0,0 +1,30 @@
package api
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tidwall/gjson"
"github.com/photoprism/photoprism/internal/i18n"
)
func TestUploadToService(t *testing.T) {
t.Run("invalid request", func(t *testing.T) {
app, router, _ := NewApiTest()
UploadToService(router)
r := PerformRequest(app, "POST", "/api/v1/services/1000000/upload")
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, i18n.Msg(i18n.ErrBadRequest), val.String())
assert.Equal(t, http.StatusBadRequest, r.Code)
})
t.Run("account not found", func(t *testing.T) {
app, router, _ := NewApiTest()
UploadToService(router)
r := PerformRequest(app, "POST", "/api/v1/services/999000/upload")
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, i18n.Msg(i18n.ErrAccountNotFound), val.String())
assert.Equal(t, http.StatusNotFound, r.Code)
})
}

View File

@@ -14,9 +14,9 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/search"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
@@ -28,7 +28,7 @@ import (
// TODO: Proof of concept, needs refactoring.
func SharePreview(router *gin.RouterGroup) {
router.GET("/:token/:shared/preview", func(c *gin.Context) {
conf := service.Config()
conf := get.Config()
token := clean.Token(c.Param("token"))
shared := clean.Token(c.Param("shared"))

View File

@@ -8,9 +8,9 @@ import (
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/crop"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
@@ -35,7 +35,7 @@ func GetThumb(router *gin.RouterGroup) {
logPrefix := "thumb"
start := time.Now()
conf := service.Config()
conf := get.Config()
download := c.Query("download") != ""
fileHash, cropArea := crop.ParseThumb(clean.Token(c.Param("thumb")))
@@ -93,7 +93,7 @@ func GetThumb(router *gin.RouterGroup) {
}
}
cache := service.ThumbCache()
cache := get.ThumbCache()
cacheKey := CacheKey("thumbs", fileHash, string(sizeName))
if cacheData, ok := cache.Get(cacheKey); ok {
@@ -196,7 +196,7 @@ func GetThumb(router *gin.RouterGroup) {
thumbName, err = size.FromCache(fileName, f.FileHash, conf.ThumbCachePath())
}
// Failed?
// RunFailed?
if err != nil {
log.Errorf("%s: %s", logPrefix, err)
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)

View File

@@ -10,8 +10,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/clean"
)
@@ -20,7 +20,7 @@ import (
// POST /api/v1/upload/:path
func Upload(router *gin.RouterGroup) {
router.POST("/upload/:token", func(c *gin.Context) {
conf := service.Config()
conf := get.Config()
if conf.ReadOnly() || !conf.Settings().Features.Upload {
Abort(c, http.StatusForbidden, i18n.ErrReadOnly)
@@ -74,7 +74,7 @@ func Upload(router *gin.RouterGroup) {
}
if !conf.UploadNSFW() {
nd := service.NsfwDetector()
nd := get.NsfwDetector()
containsNSFW := false

View File

@@ -9,9 +9,9 @@ import (
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/clean"
)
@@ -72,7 +72,7 @@ func GetVideo(router *gin.RouterGroup) {
supported := f.FileCodec != "" && f.FileCodec == string(format.Codec) || format.Codec == video.UnknownCodec && f.FileType == string(format.File)
// File bitrate too high (for streaming)?
conf := service.Config()
conf := get.Config()
transcode := !supported || conf.FFmpegEnabled() && conf.FFmpegBitrateExceeded(fileBitrate)
if mf, err := photoprism.NewMediaFile(fileName); err != nil {
@@ -81,7 +81,7 @@ func GetVideo(router *gin.RouterGroup) {
// Log error and default to 404.mp4
log.Errorf("video: file %s is missing", clean.Log(f.FileName))
fileName = service.Config().StaticFile("video/404.mp4")
fileName = get.Config().StaticFile("video/404.mp4")
AddContentTypeHeader(c, ContentTypeAvc)
} else if transcode {
if f.FileCodec != "" {
@@ -90,12 +90,12 @@ func GetVideo(router *gin.RouterGroup) {
log.Debugf("video: %s cannot be streamed directly, average bitrate %.1f MBit/s", clean.Log(f.FileName), fileBitrate)
}
conv := service.Convert()
conv := get.Convert()
if avcFile, err := conv.ToAvc(mf, service.Config().FFmpegEncoder(), false, false); err != nil {
if avcFile, err := conv.ToAvc(mf, get.Config().FFmpegEncoder(), false, false); err != nil {
// Log error and default to 404.mp4
log.Errorf("video: transcoding %s failed", clean.Log(f.FileName))
fileName = service.Config().StaticFile("video/404.mp4")
fileName = get.Config().StaticFile("video/404.mp4")
} else {
fileName = avcFile.FileName()
}

View File

@@ -15,10 +15,10 @@ import (
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/rnd"
@@ -35,7 +35,7 @@ func ZipCreate(router *gin.RouterGroup) {
return
}
conf := service.Config()
conf := get.Config()
if !conf.Settings().Features.Download {
AbortFeatureDisabled(c)
@@ -149,7 +149,7 @@ func ZipDownload(router *gin.RouterGroup) {
return
}
conf := service.Config()
conf := get.Config()
zipBaseName := clean.FileName(filepath.Base(c.Param("filename")))
zipPath := path.Join(conf.TempPath(), "zip")
zipFileName := path.Join(zipPath, zipBaseName)

View File

@@ -8,10 +8,10 @@ import (
"github.com/photoprism/photoprism/internal/api"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/clean"
)
@@ -52,7 +52,7 @@ func Import() error {
return nil
}
conf := service.Config()
conf := get.Config()
if conf.ReadOnly() || !conf.Settings().Features.Import {
return nil
@@ -62,7 +62,7 @@ func Import() error {
path := filepath.Clean(conf.ImportPath())
imp := service.Import()
imp := get.Import()
api.RemoveFromFolderCache(entity.RootImport)
@@ -82,7 +82,7 @@ func Import() error {
return nil
}
moments := service.Moments()
moments := get.Moments()
if err := moments.Start(); err != nil {
log.Warnf("moments: %s", err)

View File

@@ -8,10 +8,10 @@ import (
"github.com/photoprism/photoprism/internal/api"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/service"
)
var autoIndex = time.Time{}
@@ -51,14 +51,14 @@ func Index() error {
return nil
}
conf := service.Config()
conf := get.Config()
settings := conf.Settings()
start := time.Now()
path := conf.OriginalsPath()
ind := service.Index()
ind := get.Index()
convert := settings.Index.Convert && conf.SidecarWritable()
indOpt := photoprism.NewIndexOptions(entity.RootPath, false, convert, true, false, true)
@@ -71,7 +71,7 @@ func Index() error {
api.RemoveFromFolderCache(entity.RootOriginals)
prg := service.Purge()
prg := get.Purge()
prgOpt := photoprism.PurgeOptions{
Path: filepath.Clean(entity.RootPath),
@@ -88,7 +88,7 @@ func Index() error {
"step": "moments",
})
moments := service.Moments()
moments := get.Moments()
if err := moments.Start(); err != nil {
log.Warnf("moments: %s", err)

View File

@@ -7,8 +7,8 @@ import (
"github.com/dustin/go-humanize/english"
"github.com/urfave/cli"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/service"
)
// CleanUpCommand registers the cleanup command.
@@ -46,7 +46,7 @@ func cleanUpAction(ctx *cli.Context) error {
log.Infof("config: read-only mode enabled")
}
w := service.CleanUp()
w := get.CleanUp()
opt := photoprism.CleanUpOptions{
Dry: ctx.Bool("dry"),

View File

@@ -76,14 +76,14 @@ func childAlreadyRunning(filePath string) (pid int, running bool) {
pid, err := daemon.ReadPidFile(filePath)
// Failed?
// RunFailed?
if err != nil {
return pid, false
}
process, err := os.FindProcess(pid)
// Failed?
// RunFailed?
if err != nil {
return pid, false
}

View File

@@ -9,7 +9,7 @@ import (
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/get"
)
func TestMain(m *testing.M) {
@@ -18,7 +18,7 @@ func TestMain(m *testing.M) {
event.AuditLog = log
c := config.NewTestConfig("commands")
service.SetConfig(c)
get.SetConfig(c)
InitConfig = func(ctx *cli.Context) (*config.Config, error) {
return c, c.Init()

View File

@@ -4,12 +4,12 @@ import (
"github.com/urfave/cli"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/get"
)
// InitConfig initializes the command config.
var InitConfig = func(ctx *cli.Context) (*config.Config, error) {
c := config.NewConfig(ctx)
service.SetConfig(c)
get.SetConfig(c)
return c, c.Init()
}

View File

@@ -9,7 +9,7 @@ import (
"github.com/urfave/cli"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/pkg/clean"
)
@@ -58,7 +58,7 @@ func convertAction(ctx *cli.Context) error {
log.Infof("converting originals in %s", clean.Log(convertPath))
w := service.Convert()
w := get.Convert()
// Start file conversion.
if err := w.Start(convertPath, ctx.Bool("force")); err != nil {

View File

@@ -10,8 +10,8 @@ import (
"github.com/urfave/cli"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/clean"
)
@@ -79,7 +79,7 @@ func copyAction(ctx *cli.Context) error {
log.Infof("copying media files from %s to %s", sourcePath, filepath.Join(conf.OriginalsPath(), destFolder))
w := service.Import()
w := get.Import()
opt := photoprism.ImportOptionsCopy(sourcePath, destFolder)
w.Start(opt)

View File

@@ -11,9 +11,9 @@ import (
"github.com/urfave/cli"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
)
@@ -91,7 +91,7 @@ func facesStatsAction(ctx *cli.Context) error {
conf.InitDb()
defer conf.Shutdown()
w := service.Faces()
w := get.Faces()
if err := w.Stats(); err != nil {
return err
@@ -109,7 +109,7 @@ func facesAuditAction(ctx *cli.Context) error {
start := time.Now()
conf := config.NewConfig(ctx)
service.SetConfig(conf)
get.SetConfig(conf)
_, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -121,7 +121,7 @@ func facesAuditAction(ctx *cli.Context) error {
conf.InitDb()
defer conf.Shutdown()
w := service.Faces()
w := get.Faces()
if err := w.Audit(ctx.Bool("fix")); err != nil {
return err
@@ -152,7 +152,7 @@ func facesResetAction(ctx *cli.Context) error {
start := time.Now()
conf := config.NewConfig(ctx)
service.SetConfig(conf)
get.SetConfig(conf)
_, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -164,7 +164,7 @@ func facesResetAction(ctx *cli.Context) error {
conf.InitDb()
defer conf.Shutdown()
w := service.Faces()
w := get.Faces()
if err := w.Reset(); err != nil {
return err
@@ -191,7 +191,7 @@ func facesResetAllAction(ctx *cli.Context) error {
start := time.Now()
conf := config.NewConfig(ctx)
service.SetConfig(conf)
get.SetConfig(conf)
_, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -219,7 +219,7 @@ func facesIndexAction(ctx *cli.Context) error {
start := time.Now()
conf := config.NewConfig(ctx)
service.SetConfig(conf)
get.SetConfig(conf)
_, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -248,14 +248,14 @@ func facesIndexAction(ctx *cli.Context) error {
settings := conf.Settings()
if w := service.Index(); w != nil {
if w := get.Index(); w != nil {
convert := settings.Index.Convert && conf.SidecarWritable()
opt := photoprism.NewIndexOptions(subPath, true, convert, true, true, true)
indexed = w.Start(opt)
}
if w := service.Purge(); w != nil {
if w := get.Purge(); w != nil {
opt := photoprism.PurgeOptions{
Path: subPath,
Ignore: indexed,
@@ -280,7 +280,7 @@ func facesUpdateAction(ctx *cli.Context) error {
start := time.Now()
conf := config.NewConfig(ctx)
service.SetConfig(conf)
get.SetConfig(conf)
_, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -296,7 +296,7 @@ func facesUpdateAction(ctx *cli.Context) error {
Force: ctx.Bool("force"),
}
w := service.Faces()
w := get.Faces()
if err := w.Start(opt); err != nil {
return err
@@ -314,7 +314,7 @@ func facesOptimizeAction(ctx *cli.Context) error {
start := time.Now()
conf := config.NewConfig(ctx)
service.SetConfig(conf)
get.SetConfig(conf)
_, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -326,7 +326,7 @@ func facesOptimizeAction(ctx *cli.Context) error {
conf.InitDb()
defer conf.Shutdown()
w := service.Faces()
w := get.Faces()
if res, err := w.Optimize(); err != nil {
return err

View File

@@ -10,8 +10,8 @@ import (
"github.com/urfave/cli"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/clean"
)
@@ -79,7 +79,7 @@ func importAction(ctx *cli.Context) error {
log.Infof("moving media files from %s to %s", sourcePath, filepath.Join(conf.OriginalsPath(), destFolder))
w := service.Import()
w := get.Import()
opt := photoprism.ImportOptionsMove(sourcePath, destFolder)
w.Start(opt)

View File

@@ -9,8 +9,8 @@ import (
"github.com/dustin/go-humanize/english"
"github.com/urfave/cli"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
)
@@ -70,14 +70,14 @@ func indexAction(ctx *cli.Context) error {
var indexed fs.Done
if w := service.Index(); w != nil {
if w := get.Index(); w != nil {
convert := conf.Settings().Index.Convert && conf.SidecarWritable()
opt := photoprism.NewIndexOptions(subPath, ctx.Bool("force"), convert, true, false, !ctx.Bool("archived"))
indexed = w.Start(opt)
}
if w := service.Purge(); w != nil {
if w := get.Purge(); w != nil {
purgeStart := time.Now()
opt := photoprism.PurgeOptions{
Path: subPath,
@@ -93,7 +93,7 @@ func indexAction(ctx *cli.Context) error {
if ctx.Bool("cleanup") {
cleanupStart := time.Now()
w := service.CleanUp()
w := get.CleanUp()
opt := photoprism.CleanUpOptions{
Dry: false,

View File

@@ -3,6 +3,8 @@ package commands
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"time"
@@ -17,7 +19,7 @@ import (
var MigrationsStatusCommand = cli.Command{
Name: "ls",
Aliases: []string{"status", "show"},
Usage: "Lists the status of schema migrations",
Usage: "Displays the status of schema migrations",
ArgsUsage: "[migrations...]",
Flags: report.CliFlags,
Action: migrationsStatusAction,
@@ -81,13 +83,19 @@ func migrationsStatusAction(ctx *cli.Context) error {
}
// Report columns.
cols := []string{"ID", "Dialect", "Started At", "Finished At", "Status"}
cols := []string{"ID", "Dialect", "Stage", "Started At", "Finished At", "Status"}
// Report rows.
rows := make([][]string, 0, len(status))
for _, m := range status {
var started, finished, info string
var stage, started, finished, info string
if m.Stage == "" {
stage = "main"
} else {
stage = m.Stage
}
if m.StartedAt.IsZero() {
started = "-"
@@ -113,7 +121,7 @@ func migrationsStatusAction(ctx *cli.Context) error {
info = "Running?"
}
rows = append(rows, []string{m.ID, m.Dialect, started, finished, info})
rows = append(rows, []string{m.ID, m.Dialect, stage, started, finished, info})
}
// Display report.
@@ -130,6 +138,10 @@ func migrationsStatusAction(ctx *cli.Context) error {
// migrationsRunAction executes database schema migrations.
func migrationsRunAction(ctx *cli.Context) error {
if ctx.Args().First() == "ls" {
return fmt.Errorf("run '%s migrations ls' to display the status of schema migrations", filepath.Base(os.Args[0]))
}
start := time.Now()
conf := config.NewConfig(ctx)

View File

@@ -6,7 +6,7 @@ import (
"github.com/urfave/cli"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/get"
)
// MomentsCommand registers the moments command.
@@ -36,7 +36,7 @@ func momentsAction(ctx *cli.Context) error {
log.Infof("config: read-only mode enabled")
}
w := service.Moments()
w := get.Moments()
if err := w.Start(); err != nil {
return err

View File

@@ -9,8 +9,8 @@ import (
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
)
// PlacesCommand registers the places subcommands.
@@ -68,7 +68,7 @@ func placesUpdateAction(ctx *cli.Context) error {
start := time.Now()
// Run places worker.
if w := service.Places(); w != nil {
if w := get.Places(); w != nil {
_, err := w.Start()
if err != nil {
@@ -77,7 +77,7 @@ func placesUpdateAction(ctx *cli.Context) error {
}
// Run moments worker.
if w := service.Moments(); w != nil {
if w := get.Moments(); w != nil {
err := w.Start()
if err != nil {

View File

@@ -9,8 +9,8 @@ import (
"github.com/dustin/go-humanize/english"
"github.com/urfave/cli"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
)
@@ -63,7 +63,7 @@ func purgeAction(ctx *cli.Context) error {
log.Infof("config: read-only mode enabled")
}
w := service.Purge()
w := get.Purge()
opt := photoprism.PurgeOptions{
Path: subPath,

View File

@@ -8,6 +8,8 @@ import (
"regexp"
"time"
"github.com/photoprism/photoprism/internal/migrate"
"github.com/manifoldco/promptui"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
@@ -175,7 +177,7 @@ func resetIndexDb(c *config.Config) {
tables.Drop(c.Db())
log.Infoln("restoring default schema")
entity.InitDb(true, false, nil)
entity.InitDb(migrate.Opt(false, nil))
// Reset admin account?
if c.AdminPassword() == "" {

View File

@@ -16,8 +16,8 @@ import (
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
)
@@ -198,7 +198,7 @@ func restoreAction(ctx *cli.Context) error {
conf.InitDb()
if restoreAlbums {
service.SetConfig(conf)
get.SetConfig(conf)
if albumsPath == "" {
albumsPath = conf.AlbumsPath()

View File

@@ -7,7 +7,7 @@ import (
"github.com/urfave/cli"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/pkg/report"
)
@@ -30,7 +30,7 @@ var ConfigReports = []Report{
func showConfigAction(ctx *cli.Context) error {
conf := config.NewConfig(ctx)
conf.SetLogLevel(logrus.FatalLevel)
service.SetConfig(conf)
get.SetConfig(conf)
if err := conf.Init(); err != nil {
log.Debug(err)

View File

@@ -6,7 +6,7 @@ import (
"github.com/urfave/cli"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/pkg/clean"
)
@@ -45,7 +45,7 @@ func thumbsAction(ctx *cli.Context) error {
log.Infof("creating thumbs in %s", clean.Log(conf.ThumbCachePath()))
rs := service.Thumbs()
rs := get.Thumbs()
if err := rs.Start(ctx.Bool("force"), ctx.Bool("originals")); err != nil {
log.Error(err)

View File

@@ -95,8 +95,7 @@ func TestConfig_ClientRoleConfig(t *testing.T) {
assert.Equal(t, adminFeatures, f)
expected := customize.FeatureSettings{
Account: false,
Advanced: false,
Account: true,
Albums: true,
Archive: true,
Delete: false,
@@ -120,7 +119,7 @@ func TestConfig_ClientRoleConfig(t *testing.T) {
Search: true,
Settings: true,
Share: true,
Sync: true,
Services: true,
Upload: true,
Videos: true,
}
@@ -135,7 +134,6 @@ func TestConfig_ClientRoleConfig(t *testing.T) {
expected := customize.FeatureSettings{
Account: false,
Advanced: false,
Albums: true,
Archive: false,
Delete: false,
@@ -159,7 +157,7 @@ func TestConfig_ClientRoleConfig(t *testing.T) {
Search: false,
Settings: false,
Share: false,
Sync: false,
Services: false,
Upload: false,
Videos: false,
}
@@ -180,10 +178,9 @@ func TestConfig_ClientRoleConfig(t *testing.T) {
assert.False(t, f.Settings)
assert.False(t, f.Edit)
assert.False(t, f.Private)
assert.False(t, f.Advanced)
assert.False(t, f.Upload)
assert.False(t, f.Download)
assert.False(t, f.Sync)
assert.False(t, f.Services)
assert.False(t, f.Delete)
assert.False(t, f.Import)
assert.False(t, f.Library)
@@ -223,10 +220,9 @@ func TestConfig_ClientSessionConfig(t *testing.T) {
assert.True(t, f.Settings)
assert.True(t, f.Edit)
assert.True(t, f.Private)
assert.False(t, f.Advanced)
assert.True(t, f.Upload)
assert.True(t, f.Download)
assert.True(t, f.Sync)
assert.True(t, f.Services)
assert.False(t, f.Delete)
assert.True(t, f.Import)
assert.True(t, f.Library)
@@ -255,10 +251,9 @@ func TestConfig_ClientSessionConfig(t *testing.T) {
assert.False(t, f.Settings)
assert.False(t, f.Edit)
assert.False(t, f.Private)
assert.False(t, f.Advanced)
assert.False(t, f.Upload)
assert.True(t, f.Download)
assert.False(t, f.Sync)
assert.False(t, f.Services)
assert.False(t, f.Delete)
assert.False(t, f.Import)
assert.False(t, f.Library)
@@ -288,10 +283,9 @@ func TestConfig_ClientSessionConfig(t *testing.T) {
assert.False(t, f.Settings)
assert.False(t, f.Edit)
assert.False(t, f.Private)
assert.False(t, f.Advanced)
assert.False(t, f.Upload)
assert.False(t, f.Download)
assert.False(t, f.Sync)
assert.False(t, f.Services)
assert.False(t, f.Delete)
assert.False(t, f.Import)
assert.False(t, f.Library)
@@ -317,10 +311,9 @@ func TestConfig_ClientSessionConfig(t *testing.T) {
assert.True(t, f.Settings)
assert.True(t, f.Edit)
assert.True(t, f.Private)
assert.False(t, f.Advanced)
assert.True(t, f.Upload)
assert.True(t, f.Download)
assert.True(t, f.Sync)
assert.True(t, f.Services)
assert.False(t, f.Delete)
assert.True(t, f.Import)
assert.True(t, f.Library)

View File

@@ -10,6 +10,8 @@ import (
"strings"
"time"
"github.com/photoprism/photoprism/internal/migrate"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/sqlite"
@@ -281,7 +283,7 @@ func (c *Config) InitDb() {
// MigrateDb initializes the database and migrates the schema if needed.
func (c *Config) MigrateDb(runFailed bool, ids []string) {
entity.Admin.UserName = c.AdminUser()
entity.InitDb(true, runFailed, ids)
entity.InitDb(migrate.Opt(runFailed, ids))
// Init admin account?
if c.AdminPassword() == "" {

Some files were not shown because too many files have changed in this diff Show More