Improve link sharing dialog and api #18
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
parent
dc28b35b71
commit
722d7dd421
38 changed files with 1651 additions and 1799 deletions
2211
frontend/package-lock.json
generated
2211
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -18,17 +18,17 @@
|
|||
"gettext-compile": "gettext-compile --output src/resources/translations.json src/resources/*.po"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/cli": "^7.10.1",
|
||||
"@babel/core": "^7.10.2",
|
||||
"@babel/plugin-transform-runtime": "^7.10.1",
|
||||
"@babel/cli": "^7.10.3",
|
||||
"@babel/core": "^7.10.3",
|
||||
"@babel/plugin-transform-runtime": "^7.10.3",
|
||||
"@babel/polyfill": "^7.10.1",
|
||||
"@babel/preset-env": "^7.10.2",
|
||||
"@babel/register": "^7.10.1",
|
||||
"@babel/runtime": "^7.10.2",
|
||||
"@fortawesome/fontawesome-free": "^5.13.0",
|
||||
"@babel/preset-env": "^7.10.3",
|
||||
"@babel/register": "^7.10.3",
|
||||
"@babel/runtime": "^7.10.3",
|
||||
"@fortawesome/fontawesome-free": "^5.13.1",
|
||||
"acorn": "6.4.1",
|
||||
"ajv": "^6.12.2",
|
||||
"autoprefixer": "^9.8.0",
|
||||
"autoprefixer": "^9.8.2",
|
||||
"axios": "^0.19.2",
|
||||
"axios-mock-adapter": "^1.18.1",
|
||||
"babel-eslint": "^10.1.0",
|
||||
|
@ -99,7 +99,7 @@
|
|||
"sass-loader": "^7.3.1",
|
||||
"sinon": "^9.0.2",
|
||||
"sockette": "^2.0.6",
|
||||
"string-strip-html": "^4.4.7",
|
||||
"string-strip-html": "^4.5.0",
|
||||
"style-loader": "^0.23.1",
|
||||
"sugarss": "^2.0.0",
|
||||
"svg-url-loader": "^5.0.1",
|
||||
|
@ -121,7 +121,7 @@
|
|||
"vuetify": "^1.5.24",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-bundle-analyzer": "^3.8.0",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"webpack-cli": "^3.3.12",
|
||||
"webpack-hot-middleware": "^2.25.0",
|
||||
"webpack-md5-hash": "0.0.6",
|
||||
"webpack-merge": "^4.2.2"
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
fab dark small
|
||||
:title="labels.share"
|
||||
color="share"
|
||||
@click.stop="dialog.share = true"
|
||||
@click.stop="share()"
|
||||
:disabled="selection.length !== 1"
|
||||
v-if="$config.feature('share')"
|
||||
class="action-share"
|
||||
|
@ -75,8 +75,8 @@
|
|||
@confirm="cloneAlbums"></p-photo-album-dialog>
|
||||
<p-album-delete-dialog :show="dialog.delete" @cancel="dialog.delete = false"
|
||||
@confirm="batchDelete"></p-album-delete-dialog>
|
||||
<p-share-dialog :show="dialog.share" title="Share Album" :selection="selection" @upload="upload"
|
||||
@close="dialog.share = false"></p-share-dialog>
|
||||
<p-share-dialog :show="dialog.share" title="Share Album" :model="model" @upload="upload"
|
||||
@close="dialog.share = false"></p-share-dialog>
|
||||
<p-share-upload-dialog :show="dialog.upload" :selection="selection" @cancel="dialog.upload = false"
|
||||
@confirm="dialog.upload = false"></p-share-upload-dialog>
|
||||
</div>
|
||||
|
@ -84,6 +84,7 @@
|
|||
<script>
|
||||
import Api from "common/api";
|
||||
import Notify from "common/notify";
|
||||
import Album from "model/album";
|
||||
|
||||
export default {
|
||||
name: 'p-album-clipboard',
|
||||
|
@ -95,6 +96,7 @@
|
|||
data() {
|
||||
return {
|
||||
expanded: false,
|
||||
model: new Album(),
|
||||
dialog: {
|
||||
delete: false,
|
||||
album: false,
|
||||
|
@ -112,6 +114,20 @@
|
|||
};
|
||||
},
|
||||
methods: {
|
||||
share() {
|
||||
if (this.selection.length !== 1) {
|
||||
this.$notify.error("select one album to share");
|
||||
return;
|
||||
}
|
||||
|
||||
this.model = new Album();
|
||||
this.model.find(this.selection[0]).then(
|
||||
(m) => {
|
||||
this.model = m;
|
||||
this.dialog.share = true;
|
||||
}
|
||||
);
|
||||
},
|
||||
upload() {
|
||||
this.dialog.share = false;
|
||||
this.dialog.upload = true;
|
||||
|
|
|
@ -107,7 +107,7 @@ main {
|
|||
}
|
||||
|
||||
#photoprism .p-log-empty,
|
||||
#photoprism .p-log-message{
|
||||
#photoprism .p-log-message {
|
||||
display: block;
|
||||
text-align: left;
|
||||
font-size: 1em;
|
||||
|
@ -188,8 +188,7 @@ table.v-table tbody td input,
|
|||
table.v-table tbody td label.v-label,
|
||||
table.v-table tfoot td,
|
||||
table.v-table tfoot td input,
|
||||
table.v-table tfoot td label.v-label
|
||||
{
|
||||
table.v-table tfoot td label.v-label {
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
@ -238,3 +237,7 @@ table.v-table tfoot td div.v-input__slot {
|
|||
#photoprism .opacity-100 {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
#photoprism button:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
|
|
@ -3,56 +3,82 @@
|
|||
<v-card raised elevation="24">
|
||||
<v-card-title primary-title class="pb-0">
|
||||
<v-layout row wrap>
|
||||
<v-flex xs9>
|
||||
<h3 class="headline mb-0">{{ title }}</h3>
|
||||
<v-flex xs8>
|
||||
<h3 class="headline mb-0">Share<span class="hidden-xs-only"> {{model.modelName()}}</span></h3>
|
||||
</v-flex>
|
||||
<v-flex xs3 text-xs-right>
|
||||
<v-btn small depressed color="secondary-light" class="ma-0">
|
||||
Add Link
|
||||
<v-flex xs4 text-xs-right>
|
||||
<v-btn small depressed dark color="secondary-dark" class="ma-0" @click.stop="add()">
|
||||
<translate>Add Link</translate>
|
||||
</v-btn>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-expansion-panel class="pa-0 elevation-0">
|
||||
<v-expansion-panel-content class="pa-0 elevation-0 secondary-light">
|
||||
<v-expansion-panel-content v-for="(link, index) in links" :key="index"
|
||||
class="pa-0 elevation-0 secondary-light mb-1">
|
||||
<template v-slot:header>
|
||||
<div class="action-url">
|
||||
{{ host + '/s/a4bey45buvhg8vnxo4y'}}
|
||||
</div>
|
||||
<button class="text-xs-left action-url ml-0 mt-0 mb-0 pa-0 mr-2" @click.stop="copyUrl(link)">
|
||||
<v-icon size="16" class="pr-1">link</v-icon>
|
||||
/s/<strong style="font-weight: 500;" v-if="link.ShareToken">{{ link.ShareToken.toLowerCase() }}</strong><span v-else>...</span>
|
||||
</button>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-card-text class="grey lighten-4">
|
||||
<v-container fluid class="pa-0">
|
||||
<v-layout row wrap>
|
||||
<v-flex xs12 sm6 class="pa-2">
|
||||
<v-flex xs12 class="pa-2">
|
||||
<v-text-field
|
||||
hide-details
|
||||
:label="$gettext('URL')"
|
||||
browser-autocomplete="off"
|
||||
label="Token"
|
||||
placeholder="a4bey45buvhg8vnxo4y"
|
||||
hide-details readonly
|
||||
color="secondary-dark"
|
||||
v-model="model.AccURL"
|
||||
></v-text-field>
|
||||
@click.stop="selectText($event)"
|
||||
v-model="link.url()">
|
||||
</v-text-field>
|
||||
</v-flex>
|
||||
<v-flex xs12 sm6 class="pa-2">
|
||||
<v-select
|
||||
:label="expires(link)"
|
||||
browser-autocomplete="off"
|
||||
hide-details
|
||||
color="secondary-dark"
|
||||
item-text="text"
|
||||
item-value="value"
|
||||
v-model="link.ShareExpires"
|
||||
:items="items.expires">
|
||||
</v-select>
|
||||
</v-flex>
|
||||
<v-flex xs12 sm6 class="pa-2">
|
||||
<v-text-field
|
||||
hide-details required
|
||||
browser-autocomplete="off"
|
||||
:label="$gettext('Secret')"
|
||||
:placeholder="$gettext('Token')"
|
||||
color="secondary-dark"
|
||||
v-model="link.ShareToken"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
<!-- v-flex xs12 sm6 class="pa-2">
|
||||
<v-text-field
|
||||
hide-details
|
||||
browser-autocomplete="off"
|
||||
:label="label.pass"
|
||||
placeholder="optional"
|
||||
:placeholder="link.HasPassword ? '••••••••' : 'optional'"
|
||||
color="secondary-dark"
|
||||
v-model="model.AccPass"
|
||||
v-model="link.Password"
|
||||
:append-icon="showPassword ? 'visibility' : 'visibility_off'"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
@click:append="showPassword = !showPassword"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
</v-flex -->
|
||||
<v-flex xs12 text-xs-right class="pa-2">
|
||||
<v-btn small flat color="remove" class="ma-0">
|
||||
Delete
|
||||
<v-btn small flat color="remove" class="ma-0"
|
||||
@click.stop.exact="remove(index)">
|
||||
<v-icon>delete</v-icon>
|
||||
</v-btn>
|
||||
<v-btn small depressed dark color="secondary-dark" class="ma-0">
|
||||
<v-btn small depressed dark color="secondary-dark" class="ma-0"
|
||||
@click.stop.exact="update(link)">
|
||||
Save
|
||||
</v-btn>
|
||||
</v-flex>
|
||||
|
@ -62,31 +88,40 @@
|
|||
</v-card>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-container fluid text-xs-right class="pt-0 pb-2 pr-2 pl-2">
|
||||
<v-btn @click.stop="upload" depressed color="secondary-light"
|
||||
class="action-webdav">
|
||||
<v-icon left>cloud</v-icon>
|
||||
<span>Upload</span>
|
||||
</v-btn>
|
||||
<v-btn depressed dark color="secondary-dark" @click.stop="confirm"
|
||||
class="action-close">
|
||||
<span>{{ label.confirm }}</span>
|
||||
</v-btn>
|
||||
|
||||
<v-container fluid text-xs-left class="pb-0 pt-3 pr-0 pl-0 caption">
|
||||
People you share a link with will be able to view the {{model.modelName().toLowerCase()}}.
|
||||
A click will copy it to your clipboard. Any private photos remain private.
|
||||
Alternatively, you can upload files directly to WebDAV servers like Nextcloud.
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions class="pt-0">
|
||||
<v-layout row wrap class="pa-2">
|
||||
<v-flex xs6>
|
||||
<v-btn @click.stop="upload" depressed color="secondary-light"
|
||||
class="action-webdav">
|
||||
<span>WebDAV Upload</span>
|
||||
</v-btn>
|
||||
</v-flex>
|
||||
<v-flex xs6 text-xs-right>
|
||||
<v-btn depressed color="secondary-light" @click.stop="confirm"
|
||||
class="action-close">
|
||||
<translate>Close</translate>
|
||||
</v-btn>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<script>
|
||||
import Album from "model/album";
|
||||
|
||||
export default {
|
||||
name: 'p-share-dialog',
|
||||
props: {
|
||||
show: Boolean,
|
||||
title: String,
|
||||
model: Object,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -94,7 +129,19 @@
|
|||
showPassword: false,
|
||||
loading: false,
|
||||
search: null,
|
||||
model: new Album(),
|
||||
links: [],
|
||||
items: {
|
||||
expires: [
|
||||
{"value": 0, "text": "Never"},
|
||||
{"value": 86400, "text": "After 1 day"},
|
||||
{"value": 86400 * 3, "text": "After 3 days"},
|
||||
{"value": 86400 * 7, "text": "After 7 days"},
|
||||
{"value": 86400 * 14, "text": "After two weeks"},
|
||||
{"value": 86400 * 31, "text": "After one month"},
|
||||
{"value": 86400 * 60, "text": "After two months"},
|
||||
{"value": 86400 * 365, "text": "After one year"},
|
||||
],
|
||||
},
|
||||
label: {
|
||||
url: this.$gettext("Service URL"),
|
||||
user: this.$gettext("Username"),
|
||||
|
@ -105,11 +152,55 @@
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
selectText(ev) {
|
||||
if(!ev || !ev.target) {
|
||||
return;
|
||||
}
|
||||
|
||||
ev.target.select();
|
||||
},
|
||||
copyUrl(link) {
|
||||
window.navigator.clipboard.writeText(link.url())
|
||||
.then(() => this.$notify.success(this.$gettext("Copied to clipboard")), () => this.$notify.error(this.$gettext("Failed copying to clipboard")));
|
||||
},
|
||||
expires(link) {
|
||||
let result = this.$gettext('Expires');
|
||||
|
||||
if (link.ShareExpires <= 0) {
|
||||
return result
|
||||
}
|
||||
|
||||
return `${result}: ${link.expires()}`;
|
||||
},
|
||||
add() {
|
||||
this.loading = true;
|
||||
|
||||
this.model.addLink().then((a) => {
|
||||
this.$emit('close');
|
||||
this.model.createLink().then((r) => {
|
||||
this.links.push(r);
|
||||
}).finally(() => this.loading = false)
|
||||
},
|
||||
update(link) {
|
||||
if (!link) {
|
||||
this.$notify.error(this.$gettext("Failed updating link"))
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
this.model.updateLink(link).finally(() => this.loading = false);
|
||||
},
|
||||
remove(index) {
|
||||
const link = this.links[index];
|
||||
|
||||
if (!link) {
|
||||
this.$notify.error(this.$gettext("Failed removing link"))
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
this.model.removeLink(link).then(() => {
|
||||
this.links.splice(index, 1);
|
||||
}).finally(() => this.loading = false)
|
||||
},
|
||||
upload() {
|
||||
|
@ -124,7 +215,16 @@
|
|||
},
|
||||
watch: {
|
||||
show: function (show) {
|
||||
this.model = this.sele
|
||||
if (show) {
|
||||
this.loading = true;
|
||||
this.model.links().then((resp) => {
|
||||
if (resp.count === 0) {
|
||||
this.add();
|
||||
} else {
|
||||
this.links = resp.models;
|
||||
}
|
||||
}).finally(() => this.loading = false);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<v-container fluid class="pb-2 pr-2 pl-2">
|
||||
<v-layout row wrap>
|
||||
<v-flex xs3 text-xs-center>
|
||||
<v-icon size="54" color="grey lighten-1">cloud</v-icon>
|
||||
<v-icon size="54" color="grey lighten-1">cloud_upload</v-icon>
|
||||
</v-flex>
|
||||
<v-flex xs9 text-xs-left align-self-center>
|
||||
<v-select
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import RestModel from "model/rest";
|
||||
import Link from "model/link";
|
||||
import Api from "common/api";
|
||||
import {DateTime} from "luxon";
|
||||
import {config} from "../session";
|
||||
|
@ -26,7 +27,6 @@ export class Album extends RestModel {
|
|||
Favorite: true,
|
||||
Private: false,
|
||||
PhotoCount: 0,
|
||||
Links: [],
|
||||
CreatedAt: "",
|
||||
UpdatedAt: "",
|
||||
};
|
||||
|
@ -96,10 +96,6 @@ export class Album extends RestModel {
|
|||
return Api.delete(this.getEntityResource() + "/like");
|
||||
}
|
||||
|
||||
addLink(password, expires) {
|
||||
return Api.post(this.getEntityResource() + "/links", {"Password": password ? password : "", "Expires": expires ? expires : 0, "CanEdit": false, "CanComment": false});
|
||||
}
|
||||
|
||||
static getCollectionResource() {
|
||||
return "albums";
|
||||
}
|
||||
|
|
|
@ -37,7 +37,6 @@ export class File extends RestModel {
|
|||
Chroma: 0,
|
||||
Notes: "",
|
||||
Error: "",
|
||||
Links: [],
|
||||
CreatedAt: "",
|
||||
CreatedIn: 0,
|
||||
UpdatedAt: "",
|
||||
|
|
|
@ -27,7 +27,6 @@ export class Folder extends RestModel {
|
|||
Ignore: false,
|
||||
Watch: false,
|
||||
FileCount: 0,
|
||||
Links: [],
|
||||
CreatedAt: "",
|
||||
UpdatedAt: "",
|
||||
};
|
||||
|
|
|
@ -16,7 +16,6 @@ export class Label extends RestModel {
|
|||
Description: "",
|
||||
Notes: "",
|
||||
PhotoCount: 0,
|
||||
Links: [],
|
||||
CreatedAt: "",
|
||||
UpdatedAt: "",
|
||||
DeletedAt: "",
|
||||
|
|
|
@ -1,22 +1,71 @@
|
|||
import RestModel from "model/rest";
|
||||
import Model from "./model";
|
||||
import Api from "../common/api";
|
||||
import {DateTime} from "luxon";
|
||||
|
||||
export class Link extends RestModel {
|
||||
export default class Link extends Model {
|
||||
getDefaults() {
|
||||
return {
|
||||
Token: "",
|
||||
Password: "",
|
||||
Expires: "",
|
||||
UID: "",
|
||||
ShareUID: "",
|
||||
ShareToken: "",
|
||||
ShareExpires: 0,
|
||||
Password: "",
|
||||
HasPassword: false,
|
||||
CanComment: false,
|
||||
CanEdit: false,
|
||||
CreatedAt: "",
|
||||
UpdatedAt: "",
|
||||
Links: [],
|
||||
};
|
||||
}
|
||||
|
||||
url() {
|
||||
let token = this.ShareToken.toLowerCase();
|
||||
|
||||
if(!token) {
|
||||
token = "...";
|
||||
}
|
||||
|
||||
return `${window.location.origin}/s/${token}/${this.ShareUID}`;
|
||||
}
|
||||
|
||||
caption() {
|
||||
return `/s/${this.ShareToken.toLowerCase()}`;
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.Token;
|
||||
return this.UID;
|
||||
}
|
||||
|
||||
hasId() {
|
||||
return !!this.getId();
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new this.constructor(this.getValues());
|
||||
}
|
||||
|
||||
find(id, params) {
|
||||
return Api.get(this.getEntityResource(id), params).then((response) => Promise.resolve(new this.constructor(response.data)));
|
||||
}
|
||||
|
||||
save() {
|
||||
if (this.hasId()) {
|
||||
return this.update();
|
||||
}
|
||||
|
||||
return Api.post(this.constructor.getCollectionResource(), this.getValues()).then((response) => Promise.resolve(this.setValues(response.data)));
|
||||
}
|
||||
|
||||
update() {
|
||||
return Api.put(this.getEntityResource(), this.getValues(true)).then((response) => Promise.resolve(this.setValues(response.data)));
|
||||
}
|
||||
|
||||
remove() {
|
||||
return Api.delete(this.getEntityResource()).then(() => Promise.resolve(this));
|
||||
}
|
||||
|
||||
expires() {
|
||||
return DateTime.fromISO(this.UpdatedAt).plus({ seconds: this.ShareExpires }).toLocaleString(DateTime.DATE_SHORT);
|
||||
}
|
||||
|
||||
static getCollectionResource() {
|
||||
|
@ -27,5 +76,3 @@ export class Link extends RestModel {
|
|||
return "Link";
|
||||
}
|
||||
}
|
||||
|
||||
export default Link;
|
||||
|
|
|
@ -65,7 +65,6 @@ export class Photo extends RestModel {
|
|||
Labels: [],
|
||||
Keywords: [],
|
||||
Albums: [],
|
||||
Links: [],
|
||||
Location: {},
|
||||
Place: {},
|
||||
PlaceID: "",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Api from "common/api";
|
||||
import Form from "common/form";
|
||||
import Model from "./model";
|
||||
import Link from "./link";
|
||||
|
||||
export class Rest extends Model {
|
||||
getId() {
|
||||
|
@ -16,7 +17,7 @@ export class Rest extends Model {
|
|||
}
|
||||
|
||||
find(id, params) {
|
||||
return Api.get(this.getEntityResource(id), params).then((response) => Promise.resolve(new this.constructor(response.data)));
|
||||
return Api.get(this.getEntityResource(id), params).then((resp) => Promise.resolve(new this.constructor(resp.data)));
|
||||
}
|
||||
|
||||
save() {
|
||||
|
@ -24,11 +25,11 @@ export class Rest extends Model {
|
|||
return this.update();
|
||||
}
|
||||
|
||||
return Api.post(this.constructor.getCollectionResource(), this.getValues()).then((response) => Promise.resolve(this.setValues(response.data)));
|
||||
return Api.post(this.constructor.getCollectionResource(), this.getValues()).then((resp) => Promise.resolve(this.setValues(resp.data)));
|
||||
}
|
||||
|
||||
update() {
|
||||
return Api.put(this.getEntityResource(), this.getValues(true)).then((response) => Promise.resolve(this.setValues(response.data)));
|
||||
return Api.put(this.getEntityResource(), this.getValues(true)).then((resp) => Promise.resolve(this.setValues(resp.data)));
|
||||
}
|
||||
|
||||
remove() {
|
||||
|
@ -36,7 +37,7 @@ export class Rest extends Model {
|
|||
}
|
||||
|
||||
getEditForm() {
|
||||
return Api.options(this.getEntityResource()).then(response => Promise.resolve(new Form(response.data)));
|
||||
return Api.options(this.getEntityResource()).then(resp => Promise.resolve(new Form(resp.data)));
|
||||
}
|
||||
|
||||
getEntityResource(id) {
|
||||
|
@ -51,12 +52,50 @@ export class Rest extends Model {
|
|||
return this.constructor.getModelName() + " " + this.getId();
|
||||
}
|
||||
|
||||
addLink(password, expires, comment, edit) {
|
||||
expires = expires ? parseInt(expires) : 0;
|
||||
comment = !!comment;
|
||||
edit = !!edit;
|
||||
const values = {password, expires, comment, edit};
|
||||
return Api.post(this.getEntityResource() + "/link", values).then((response) => Promise.resolve(this.setValues(response.data)));
|
||||
createLink(password, expires) {
|
||||
return Api
|
||||
.post(this.getEntityResource() + "/links", {
|
||||
"Password": password ? password : "",
|
||||
"ShareExpires": expires ? expires : 0,
|
||||
"CanEdit": false,
|
||||
"CanComment": false
|
||||
})
|
||||
.then((resp) => Promise.resolve(new Link(resp.data)));
|
||||
}
|
||||
|
||||
updateLink(link) {
|
||||
let values = link.getValues(false);
|
||||
|
||||
if(link.Password) {
|
||||
values["Password"] = link.Password;
|
||||
}
|
||||
|
||||
return Api
|
||||
.put(this.getEntityResource() + "/links/" + link.getId(), values)
|
||||
.then((resp) => Promise.resolve(link.setValues(resp.data)));
|
||||
}
|
||||
|
||||
removeLink(link) {
|
||||
return Api
|
||||
.delete(this.getEntityResource() + "/links/" + link.getId())
|
||||
.then((resp) => Promise.resolve(link.setValues(resp.data)));
|
||||
}
|
||||
|
||||
links() {
|
||||
return Api.get(this.getEntityResource() + "/links").then((resp) => {
|
||||
resp.models = [];
|
||||
resp.count = resp.data.length;
|
||||
|
||||
for (let i = 0; i < resp.data.length; i++) {
|
||||
resp.models.push(new Link(resp.data[i]));
|
||||
}
|
||||
|
||||
return Promise.resolve(resp);
|
||||
});
|
||||
}
|
||||
|
||||
modelName() {
|
||||
return this.constructor.getModelName()
|
||||
}
|
||||
|
||||
static getCollectionResource() {
|
||||
|
@ -68,7 +107,7 @@ export class Rest extends Model {
|
|||
}
|
||||
|
||||
static getCreateForm() {
|
||||
return Api.options(this.getCreateResource()).then(response => Promise.resolve(new Form(response.data)));
|
||||
return Api.options(this.getCreateResource()).then(resp => Promise.resolve(new Form(resp.data)));
|
||||
}
|
||||
|
||||
static getModelName() {
|
||||
|
@ -76,7 +115,7 @@ export class Rest extends Model {
|
|||
}
|
||||
|
||||
static getSearchForm() {
|
||||
return Api.options(this.getCollectionResource()).then(response => Promise.resolve(new Form(response.data)));
|
||||
return Api.options(this.getCollectionResource()).then(resp => Promise.resolve(new Form(resp.data)));
|
||||
}
|
||||
|
||||
static search(params) {
|
||||
|
@ -84,35 +123,35 @@ export class Rest extends Model {
|
|||
params: params,
|
||||
};
|
||||
|
||||
return Api.get(this.getCollectionResource(), options).then((response) => {
|
||||
let count = response.data.length;
|
||||
return Api.get(this.getCollectionResource(), options).then((resp) => {
|
||||
let count = resp.data.length;
|
||||
let limit = 0;
|
||||
let offset = 0;
|
||||
|
||||
if (response.headers) {
|
||||
if (response.headers["x-count"]) {
|
||||
count = parseInt(response.headers["x-count"]);
|
||||
if (resp.headers) {
|
||||
if (resp.headers["x-count"]) {
|
||||
count = parseInt(resp.headers["x-count"]);
|
||||
}
|
||||
|
||||
if (response.headers["x-limit"]) {
|
||||
limit = parseInt(response.headers["x-limit"]);
|
||||
if (resp.headers["x-limit"]) {
|
||||
limit = parseInt(resp.headers["x-limit"]);
|
||||
}
|
||||
|
||||
if (response.headers["x-offset"]) {
|
||||
offset = parseInt(response.headers["x-offset"]);
|
||||
if (resp.headers["x-offset"]) {
|
||||
offset = parseInt(resp.headers["x-offset"]);
|
||||
}
|
||||
}
|
||||
|
||||
response.models = [];
|
||||
response.count = count;
|
||||
response.limit = limit;
|
||||
response.offset = offset;
|
||||
resp.models = [];
|
||||
resp.count = count;
|
||||
resp.limit = limit;
|
||||
resp.offset = offset;
|
||||
|
||||
for (let i = 0; i < response.data.length; i++) {
|
||||
response.models.push(new this(response.data[i]));
|
||||
for (let i = 0; i < resp.data.length; i++) {
|
||||
resp.models.push(new this(resp.data[i]));
|
||||
}
|
||||
|
||||
return Promise.resolve(response);
|
||||
return Promise.resolve(resp);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,10 +5,7 @@ import (
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// GET /api/v1/files/:hash
|
||||
|
@ -32,34 +29,3 @@ func GetFile(router *gin.RouterGroup, conf *config.Config) {
|
|||
c.JSON(http.StatusOK, p)
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/files/:uid/link
|
||||
//
|
||||
// Parameters:
|
||||
// uid: string SHA-1 hash of the file
|
||||
func LinkFile(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.POST("/files/:uid/link", func(c *gin.Context) {
|
||||
if Unauthorized(c, conf) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
m, err := query.FileByUID(c.Param("uid"))
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrFileNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if link, err := newLink(c); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
return
|
||||
} else {
|
||||
entity.Db().Model(&m).Association("Links").Append(link)
|
||||
}
|
||||
|
||||
event.Success("created file share link")
|
||||
|
||||
c.JSON(http.StatusOK, m)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/tidwall/gjson"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
@ -27,42 +26,3 @@ func TestGetFile(t *testing.T) {
|
|||
assert.Equal(t, http.StatusNotFound, r.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLinkFile(t *testing.T) {
|
||||
t.Run("successful request", func(t *testing.T) {
|
||||
app, router, ctx := NewApiTest()
|
||||
LinkFile(router, ctx)
|
||||
r := PerformRequestWithBody(app, "POST", "/api/v1/files/ft9es39w45bnlqdw/link", `{"Password": "foobar123", "Expires": 0, "CanEdit": true}`)
|
||||
|
||||
var label entity.Label
|
||||
|
||||
if err := json.Unmarshal(r.Body.Bytes(), &label); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(label.Links) != 1 {
|
||||
t.Fatalf("one link expected: %d, %+v", len(label.Links), label)
|
||||
}
|
||||
|
||||
link := label.Links[0]
|
||||
|
||||
assert.Equal(t, "foobar123", link.LinkPassword)
|
||||
assert.Nil(t, link.LinkExpires)
|
||||
assert.False(t, link.CanComment)
|
||||
assert.True(t, link.CanEdit)
|
||||
})
|
||||
t.Run("file not found", func(t *testing.T) {
|
||||
app, router, ctx := NewApiTest()
|
||||
LinkFile(router, ctx)
|
||||
r := PerformRequestWithBody(app, "POST", "/api/v1/files/xxx/link", `{"Password": "foobar", "Expires": 0, "CanEdit": true}`)
|
||||
assert.Equal(t, http.StatusNotFound, r.Code)
|
||||
val := gjson.Get(r.Body.String(), "error")
|
||||
assert.Equal(t, "File not found", val.String())
|
||||
})
|
||||
t.Run("invalid request", func(t *testing.T) {
|
||||
app, router, ctx := NewApiTest()
|
||||
LinkFile(router, ctx)
|
||||
r := PerformRequestWithBody(app, "POST", "/api/v1/files/ft9es39w45bnlqdw/link", `{"xxx": 123, "Expires": 0, "CanEdit": "xxx"}`)
|
||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package api
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
|
@ -13,32 +13,130 @@ import (
|
|||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// newLink returns a new link entity initialized with request data
|
||||
func newLink(c *gin.Context) (link entity.Link, err error) {
|
||||
// PUT /api/v1/:entity/:uid/links/:link
|
||||
func UpdateLink(c *gin.Context, conf *config.Config) {
|
||||
if Unauthorized(c, conf) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var f form.Link
|
||||
|
||||
if err := c.BindJSON(&f); err != nil {
|
||||
return link, err
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
link = entity.NewLink(f.Password, f.CanComment, f.CanEdit)
|
||||
link := entity.FindLink(c.Param("link"))
|
||||
|
||||
if f.Expires > 0 {
|
||||
expires := time.Now().Add(time.Duration(f.Expires) * time.Second)
|
||||
link.LinkExpires = &expires
|
||||
link.ShareExpires = f.ShareExpires
|
||||
|
||||
if f.ShareToken != "" {
|
||||
link.ShareToken = strings.ToLower(f.ShareToken)
|
||||
}
|
||||
|
||||
return link, nil
|
||||
if f.Password != "" {
|
||||
if err := link.SetPassword(f.Password); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := link.Save(); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
event.Success("updated share link")
|
||||
|
||||
c.JSON(http.StatusOK, link)
|
||||
}
|
||||
|
||||
// POST /api/v1/albums/:uid/link
|
||||
func LinkAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||
// DELETE /api/v1/:entity/:uid/links/:link
|
||||
func DeleteLink(c *gin.Context, conf *config.Config) {
|
||||
if Unauthorized(c, conf) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
link := entity.FindLink(c.Param("link"))
|
||||
|
||||
if err := link.Delete(); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
event.Success("deleted share link")
|
||||
|
||||
c.JSON(http.StatusOK, link)
|
||||
}
|
||||
|
||||
// CreateLink returns a new link entity initialized with request data
|
||||
func CreateLink(c *gin.Context, conf *config.Config) {
|
||||
if Unauthorized(c, conf) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var f form.Link
|
||||
|
||||
if err := c.BindJSON(&f); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
link := entity.NewLink(c.Param("uid"), f.CanComment, f.CanEdit)
|
||||
|
||||
if f.ShareExpires > 0 {
|
||||
link.ShareExpires = f.ShareExpires
|
||||
}
|
||||
|
||||
if f.Password != "" {
|
||||
if err := link.SetPassword(f.Password); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := link.Save(); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
event.Success("added share link")
|
||||
|
||||
c.JSON(http.StatusOK, link)
|
||||
}
|
||||
|
||||
// POST /api/v1/albums/:uid/links
|
||||
func CreateAlbumLink(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.POST("/albums/:uid/links", func(c *gin.Context) {
|
||||
if Unauthorized(c, conf) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
if _, err := query.AlbumByUID(c.Param("uid")); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
CreateLink(c, conf)
|
||||
})
|
||||
}
|
||||
|
||||
// PUT /api/v1/albums/:uid/links/:link
|
||||
func UpdateAlbumLink(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.PUT("/albums/:uid/links/:link", func(c *gin.Context) {
|
||||
UpdateLink(c, conf)
|
||||
})
|
||||
}
|
||||
|
||||
// DELETE /api/v1/albums/:uid/links/:link
|
||||
func DeleteAlbumLink(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.DELETE("/albums/:uid/links/:link", func(c *gin.Context) {
|
||||
DeleteLink(c, conf)
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/v1/albums/:uid/links
|
||||
func GetAlbumLinks(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.GET("/albums/:uid/links", func(c *gin.Context) {
|
||||
m, err := query.AlbumByUID(c.Param("uid"))
|
||||
|
||||
if err != nil {
|
||||
|
@ -46,71 +144,87 @@ func LinkAlbum(router *gin.RouterGroup, conf *config.Config) {
|
|||
return
|
||||
}
|
||||
|
||||
if link, err := newLink(c); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
return
|
||||
} else {
|
||||
entity.Db().Model(&m).Association("Links").Append(link)
|
||||
}
|
||||
|
||||
event.Success("created album share link")
|
||||
|
||||
c.JSON(http.StatusOK, m)
|
||||
c.JSON(http.StatusOK, m.Links())
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/photos/:uid/link
|
||||
func LinkPhoto(router *gin.RouterGroup, conf *config.Config) {
|
||||
// POST /api/v1/photos/:uid/links
|
||||
func CreatePhotoLink(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.POST("/photos/:uid/links", func(c *gin.Context) {
|
||||
if Unauthorized(c, conf) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
m, err := query.PhotoByUID(c.Param("uid"))
|
||||
|
||||
if err != nil {
|
||||
if _, err := query.PhotoByUID(c.Param("uid")); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if link, err := newLink(c); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
return
|
||||
} else {
|
||||
entity.Db().Model(&m).Association("Links").Append(link)
|
||||
}
|
||||
|
||||
event.Success("created photo share link")
|
||||
|
||||
c.JSON(http.StatusOK, m)
|
||||
CreateLink(c, conf)
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/labels/:uid/link
|
||||
func LinkLabel(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.POST("/labels/:uid/links", func(c *gin.Context) {
|
||||
if Unauthorized(c, conf) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
// PUT /api/v1/photos/:uid/links/:link
|
||||
func UpdatePhotoLink(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.PUT("/photos/:uid/links/:link", func(c *gin.Context) {
|
||||
UpdateLink(c, conf)
|
||||
})
|
||||
}
|
||||
|
||||
// DELETE /api/v1/photos/:uid/links/:link
|
||||
func DeletePhotoLink(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.DELETE("/photos/:uid/links/:link", func(c *gin.Context) {
|
||||
DeleteLink(c, conf)
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/v1/photos/:uid/links
|
||||
func GetPhotoLinks(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.GET("/photos/:uid/links", func(c *gin.Context) {
|
||||
m, err := query.PhotoByUID(c.Param("uid"))
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
m, err := query.LabelByUID(c.Param("uid"))
|
||||
c.JSON(http.StatusOK, m.Links())
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// POST /api/v1/labels/:uid/links
|
||||
func CreateLabelLink(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.POST("/labels/:uid/links", func(c *gin.Context) {
|
||||
if _, err := query.LabelByUID(c.Param("uid")); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrLabelNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if link, err := newLink(c); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
return
|
||||
} else {
|
||||
entity.Db().Model(&m).Association("Links").Append(link)
|
||||
}
|
||||
|
||||
event.Success("created label share link")
|
||||
|
||||
c.JSON(http.StatusOK, m)
|
||||
CreateLink(c, conf)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// PUT /api/v1/labels/:uid/links/:link
|
||||
func UpdateLabelLink(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.PUT("/labels/:uid/links/:link", func(c *gin.Context) {
|
||||
UpdateLink(c, conf)
|
||||
})
|
||||
}
|
||||
|
||||
// DELETE /api/v1/labels/:uid/links/:link
|
||||
func DeleteLabelLink(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.DELETE("/labels/:uid/links/:link", func(c *gin.Context) {
|
||||
DeleteLink(c, conf)
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/v1/labels/:uid/links
|
||||
func GetLabelLinks(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.GET("/labels/:uid/links", func(c *gin.Context) {
|
||||
m, err := query.LabelByUID(c.Param("uid"))
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, m.Links())
|
||||
})
|
||||
}
|
||||
|
|
|
@ -14,56 +14,50 @@ func TestLinkAlbum(t *testing.T) {
|
|||
t.Run("create share link", func(t *testing.T) {
|
||||
app, router, ctx := NewApiTest()
|
||||
|
||||
var album entity.Album
|
||||
var link entity.Link
|
||||
|
||||
LinkAlbum(router, ctx)
|
||||
CreateAlbumLink(router, ctx)
|
||||
|
||||
result1 := PerformRequestWithBody(app, "POST", "/api/v1/albums/at9lxuqxpogaaba7/links", `{"Password": "foobar", "Expires": 0, "CanEdit": true}`)
|
||||
assert.Equal(t, http.StatusOK, result1.Code)
|
||||
resp := PerformRequestWithBody(app, "POST", "/api/v1/albums/at9lxuqxpogaaba7/links", `{"Password": "foobar", "ShareExpires": 0, "CanEdit": true}`)
|
||||
|
||||
if err := json.Unmarshal(result1.Body.Bytes(), &album); err != nil {
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Fatal(resp.Body.String())
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(resp.Body.Bytes(), &link); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(album.Links) != 1 {
|
||||
t.Fatalf("one link expected: %d, %+v", len(album.Links), album)
|
||||
}
|
||||
|
||||
link := album.Links[0]
|
||||
|
||||
assert.NotEmpty(t, link.LinkUID)
|
||||
assert.NotEmpty(t, link.ShareUID)
|
||||
assert.NotEmpty(t, link.ShareToken)
|
||||
assert.Equal(t, true, link.CanEdit)
|
||||
assert.Nil(t, link.LinkExpires)
|
||||
assert.Equal(t, 0, link.ShareExpires)
|
||||
assert.False(t, link.CanComment)
|
||||
assert.True(t, link.CanEdit)
|
||||
|
||||
result2 := PerformRequestWithBody(app, "POST", "/api/v1/albums/at9lxuqxpogaaba7/links", `{"Password": "", "Expires": 3600}`)
|
||||
|
||||
assert.Equal(t, http.StatusOK, result2.Code)
|
||||
|
||||
// t.Logf("result1: %s", result1.Body.String())
|
||||
// t.Logf("result2: %s", result2.Body.String())
|
||||
|
||||
if err := json.Unmarshal(result2.Body.Bytes(), &album); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(album.Links) != 2 {
|
||||
t.Fatal("two links expected")
|
||||
}
|
||||
})
|
||||
t.Run("album not found", func(t *testing.T) {
|
||||
t.Run("album does not exist", func(t *testing.T) {
|
||||
app, router, ctx := NewApiTest()
|
||||
LinkAlbum(router, ctx)
|
||||
r := PerformRequestWithBody(app, "POST", "/api/v1/albums/xxx/links", `{"Password": "foobar", "Expires": 0, "CanEdit": true}`)
|
||||
assert.Equal(t, http.StatusNotFound, r.Code)
|
||||
val := gjson.Get(r.Body.String(), "error")
|
||||
CreateAlbumLink(router, ctx)
|
||||
resp := PerformRequestWithBody(app, "POST", "/api/v1/albums/xxx/links", `{"Password": "foobar", "ShareExpires": 0, "CanEdit": true}`)
|
||||
|
||||
if resp.Code != http.StatusNotFound {
|
||||
t.Fatal(resp.Body.String())
|
||||
}
|
||||
|
||||
val := gjson.Get(resp.Body.String(), "error")
|
||||
assert.Equal(t, "Album not found", val.String())
|
||||
})
|
||||
t.Run("invalid request", func(t *testing.T) {
|
||||
app, router, ctx := NewApiTest()
|
||||
LinkAlbum(router, ctx)
|
||||
r := PerformRequestWithBody(app, "POST", "/api/v1/albums/at9lxuqxpogaaba7/links", `{"xxx": 123, "Expires": 0, "CanEdit": "xxx"}`)
|
||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||
|
||||
CreateAlbumLink(router, ctx)
|
||||
|
||||
resp := PerformRequestWithBody(app, "POST", "/api/v1/albums/at9lxuqxpogaaba7/links", `{"Password": "foobar", "ShareExpires": "abc", "CanEdit": true}`)
|
||||
|
||||
if resp.Code != http.StatusBadRequest {
|
||||
t.Fatal(resp.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -71,52 +65,39 @@ func TestLinkPhoto(t *testing.T) {
|
|||
t.Run("create share link", func(t *testing.T) {
|
||||
app, router, ctx := NewApiTest()
|
||||
|
||||
var photo entity.Photo
|
||||
var link entity.Link
|
||||
|
||||
LinkPhoto(router, ctx)
|
||||
CreatePhotoLink(router, ctx)
|
||||
|
||||
result1 := PerformRequestWithBody(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh7/links", `{"Password": "foobar", "Expires": 0, "CanEdit": true}`)
|
||||
assert.Equal(t, http.StatusOK, result1.Code)
|
||||
resp := PerformRequestWithBody(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh7/links", `{"Password": "foobar", "ShareExpires": 0, "CanEdit": true}`)
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
if err := json.Unmarshal(result1.Body.Bytes(), &photo); err != nil {
|
||||
if err := json.Unmarshal(resp.Body.Bytes(), &link); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(photo.Links) != 1 {
|
||||
t.Fatalf("one link expected: %d, %+v", len(photo.Links), photo)
|
||||
}
|
||||
|
||||
link := photo.Links[0]
|
||||
|
||||
assert.Equal(t, "foobar", link.LinkPassword)
|
||||
assert.Nil(t, link.LinkExpires)
|
||||
assert.NotEmpty(t, link.LinkUID)
|
||||
assert.NotEmpty(t, link.ShareUID)
|
||||
assert.NotEmpty(t, link.ShareToken)
|
||||
assert.Equal(t, 0, link.ShareExpires)
|
||||
assert.False(t, link.CanComment)
|
||||
assert.True(t, link.CanEdit)
|
||||
|
||||
result2 := PerformRequestWithBody(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh7/links", `{"Password": "", "Expires": 3600}`)
|
||||
|
||||
assert.Equal(t, http.StatusOK, result2.Code)
|
||||
|
||||
if err := json.Unmarshal(result2.Body.Bytes(), &photo); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(photo.Links) != 2 {
|
||||
t.Fatal("two links expected")
|
||||
}
|
||||
})
|
||||
t.Run("photo not found", func(t *testing.T) {
|
||||
app, router, ctx := NewApiTest()
|
||||
LinkPhoto(router, ctx)
|
||||
r := PerformRequestWithBody(app, "POST", "/api/v1/photos/xxx/link", `{"Password": "foobar", "Expires": 0, "CanEdit": true}`)
|
||||
assert.Equal(t, http.StatusNotFound, r.Code)
|
||||
val := gjson.Get(r.Body.String(), "error")
|
||||
assert.Equal(t, "Photo not found", val.String())
|
||||
|
||||
CreatePhotoLink(router, ctx)
|
||||
|
||||
resp := PerformRequestWithBody(app, "POST", "/api/v1/photos/xxx/link", `{"Password": "foobar", "ShareExpires": 0, "CanEdit": true}`)
|
||||
|
||||
if resp.Code != http.StatusNotFound {
|
||||
t.Fatal(resp.Body.String())
|
||||
}
|
||||
})
|
||||
t.Run("invalid request", func(t *testing.T) {
|
||||
app, router, ctx := NewApiTest()
|
||||
LinkPhoto(router, ctx)
|
||||
r := PerformRequestWithBody(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh7/links", `{"xxx": 123, "Expires": 0, "CanEdit": "xxx"}`)
|
||||
CreatePhotoLink(router, ctx)
|
||||
r := PerformRequestWithBody(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh7/links", `{"xxx": 123, "ShareExpires": "abc", "CanEdit": "xxx"}`)
|
||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||
})
|
||||
}
|
||||
|
@ -125,52 +106,37 @@ func TestLinkLabel(t *testing.T) {
|
|||
t.Run("create share link", func(t *testing.T) {
|
||||
app, router, ctx := NewApiTest()
|
||||
|
||||
var label entity.Label
|
||||
var link entity.Link
|
||||
|
||||
LinkLabel(router, ctx)
|
||||
CreateLabelLink(router, ctx)
|
||||
|
||||
result1 := PerformRequestWithBody(app, "POST", "/api/v1/labels/lt9k3pw1wowuy3c2/links", `{"Password": "foobar", "Expires": 0, "CanEdit": true}`)
|
||||
assert.Equal(t, http.StatusOK, result1.Code)
|
||||
resp := PerformRequestWithBody(app, "POST", "/api/v1/labels/lt9k3pw1wowuy3c2/links", `{"Password": "foobar", "ShareExpires": 0, "CanEdit": true}`)
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
if err := json.Unmarshal(result1.Body.Bytes(), &label); err != nil {
|
||||
if err := json.Unmarshal(resp.Body.Bytes(), &link); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(label.Links) != 1 {
|
||||
t.Fatalf("one link expected: %d, %+v", len(label.Links), label)
|
||||
}
|
||||
|
||||
link := label.Links[0]
|
||||
|
||||
assert.Equal(t, "foobar", link.LinkPassword)
|
||||
assert.Nil(t, link.LinkExpires)
|
||||
assert.NotEmpty(t, link.LinkUID)
|
||||
assert.NotEmpty(t, link.ShareUID)
|
||||
assert.NotEmpty(t, link.ShareToken)
|
||||
assert.Equal(t, 0, link.ShareExpires)
|
||||
assert.False(t, link.CanComment)
|
||||
assert.True(t, link.CanEdit)
|
||||
|
||||
result2 := PerformRequestWithBody(app, "POST", "/api/v1/labels/lt9k3pw1wowuy3c2/links", `{"Password": "", "Expires": 3600}`)
|
||||
|
||||
assert.Equal(t, http.StatusOK, result2.Code)
|
||||
|
||||
if err := json.Unmarshal(result2.Body.Bytes(), &label); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(label.Links) != 2 {
|
||||
t.Fatal("two links expected")
|
||||
}
|
||||
})
|
||||
t.Run("label not found", func(t *testing.T) {
|
||||
app, router, ctx := NewApiTest()
|
||||
LinkLabel(router, ctx)
|
||||
r := PerformRequestWithBody(app, "POST", "/api/v1/labels/xxx/links", `{"Password": "foobar", "Expires": 0, "CanEdit": true}`)
|
||||
assert.Equal(t, http.StatusNotFound, r.Code)
|
||||
val := gjson.Get(r.Body.String(), "error")
|
||||
assert.Equal(t, "Label not found", val.String())
|
||||
CreateLabelLink(router, ctx)
|
||||
resp := PerformRequestWithBody(app, "POST", "/api/v1/labels/xxx/links", `{"Password": "foobar", "ShareExpires": 0, "CanEdit": true}`)
|
||||
|
||||
if resp.Code != http.StatusNotFound {
|
||||
t.Fatal(resp.Body.String())
|
||||
}
|
||||
})
|
||||
t.Run("invalid request", func(t *testing.T) {
|
||||
app, router, ctx := NewApiTest()
|
||||
LinkLabel(router, ctx)
|
||||
r := PerformRequestWithBody(app, "POST", "/api/v1/labels/lt9k3pw1wowuy3c2/links", `{"xxx": 123, "Expires": 0, "CanEdit": "xxx"}`)
|
||||
CreateLabelLink(router, ctx)
|
||||
r := PerformRequestWithBody(app, "POST", "/api/v1/labels/lt9k3pw1wowuy3c2/links", `{"xxx": 123, "ShareExpires": "abc", "CanEdit": "xxx"}`)
|
||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -45,7 +45,6 @@ type Album struct {
|
|||
AlbumMonth int `gorm:"index:idx_albums_country_year_month;" json:"Month" yaml:"Month,omitempty"`
|
||||
AlbumFavorite bool `json:"Favorite" yaml:"Favorite,omitempty"`
|
||||
AlbumPrivate bool `json:"Private" yaml:"Private,omitempty"`
|
||||
Links []Link `gorm:"foreignkey:share_uid;association_foreignkey:album_uid" json:"Links" yaml:"-"`
|
||||
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
|
||||
DeletedAt *time.Time `sql:"index" json:"-" yaml:"-"`
|
||||
|
@ -365,3 +364,8 @@ func (m *Album) RemovePhotos(UIDs []string) (removed []PhotoAlbum) {
|
|||
|
||||
return removed
|
||||
}
|
||||
|
||||
// Links returns all share links for this entity.
|
||||
func (m *Album) Links() Links {
|
||||
return FindLinks("", m.AlbumUID)
|
||||
}
|
||||
|
|
|
@ -35,7 +35,6 @@ var AlbumFixtures = AlbumMap{
|
|||
AlbumOrder: "oldest",
|
||||
AlbumTemplate: "",
|
||||
AlbumFavorite: false,
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
UpdatedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
DeletedAt: nil,
|
||||
|
@ -52,7 +51,6 @@ var AlbumFixtures = AlbumMap{
|
|||
AlbumOrder: "newest",
|
||||
AlbumTemplate: "",
|
||||
AlbumFavorite: true,
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Date(2019, 7, 1, 0, 0, 0, 0, time.UTC),
|
||||
UpdatedAt: time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC),
|
||||
DeletedAt: nil,
|
||||
|
@ -69,7 +67,6 @@ var AlbumFixtures = AlbumMap{
|
|||
AlbumOrder: "oldest",
|
||||
AlbumTemplate: "",
|
||||
AlbumFavorite: false,
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Date(2019, 7, 1, 0, 0, 0, 0, time.UTC),
|
||||
UpdatedAt: time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC),
|
||||
DeletedAt: nil,
|
||||
|
@ -87,7 +84,6 @@ var AlbumFixtures = AlbumMap{
|
|||
AlbumTemplate: "",
|
||||
AlbumFilter: "path:\"1990/04\"",
|
||||
AlbumFavorite: false,
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Date(2019, 7, 1, 0, 0, 0, 0, time.UTC),
|
||||
UpdatedAt: time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC),
|
||||
DeletedAt: nil,
|
||||
|
@ -105,7 +101,6 @@ var AlbumFixtures = AlbumMap{
|
|||
AlbumTemplate: "",
|
||||
AlbumFilter: "",
|
||||
AlbumFavorite: false,
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
UpdatedAt: time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC),
|
||||
DeletedAt: nil,
|
||||
|
|
|
@ -49,6 +49,7 @@ var Entities = Types{
|
|||
"photos_labels": &PhotoLabel{},
|
||||
"keywords": &Keyword{},
|
||||
"photos_keywords": &PhotoKeyword{},
|
||||
"passwords": &Password{},
|
||||
"links": &Link{},
|
||||
}
|
||||
|
||||
|
|
|
@ -51,7 +51,6 @@ type File struct {
|
|||
FileError string `gorm:"type:varbinary(512)" json:"Error" yaml:"Error,omitempty"`
|
||||
Share []FileShare `json:"-" yaml:"-"`
|
||||
Sync []FileSync `json:"-" yaml:"-"`
|
||||
Links []Link `gorm:"foreignkey:share_uid;association_foreignkey:file_uid" json:"Links" yaml:"-"`
|
||||
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
|
||||
CreatedIn int64 `json:"CreatedIn" yaml:"-"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
|
||||
|
@ -193,3 +192,8 @@ func (m *File) RelatedPhoto() *Photo {
|
|||
func (m *File) NoJPEG() bool {
|
||||
return m.FileType != string(fs.TypeJpeg)
|
||||
}
|
||||
|
||||
// Links returns all share links for this entity.
|
||||
func (m *File) Links() Links {
|
||||
return FindLinks("", m.FileUID)
|
||||
}
|
||||
|
|
|
@ -40,7 +40,6 @@ var FileFixtures = map[string]File{
|
|||
FileShareFixtures.Get("FileShare2", 0, 0, ""),
|
||||
},
|
||||
Sync: []FileSync{},
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Date(2020, 3, 6, 2, 6, 51, 0, time.UTC),
|
||||
CreatedIn: 2,
|
||||
UpdatedAt: time.Date(2020, 3, 28, 14, 6, 0, 0, time.UTC),
|
||||
|
@ -79,7 +78,6 @@ var FileFixtures = map[string]File{
|
|||
FileError: "",
|
||||
Share: []FileShare{},
|
||||
Sync: []FileSync{},
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Date(2019, 3, 6, 2, 6, 51, 0, time.UTC),
|
||||
CreatedIn: 2,
|
||||
UpdatedAt: time.Date(2020, 3, 28, 14, 6, 0, 0, time.UTC),
|
||||
|
@ -118,7 +116,6 @@ var FileFixtures = map[string]File{
|
|||
FileError: "",
|
||||
Share: []FileShare{},
|
||||
Sync: []FileSync{},
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Date(2019, 3, 6, 2, 6, 51, 0, time.UTC),
|
||||
CreatedIn: 2,
|
||||
UpdatedAt: time.Date(2020, 3, 28, 14, 6, 0, 0, time.UTC),
|
||||
|
@ -157,7 +154,6 @@ var FileFixtures = map[string]File{
|
|||
FileError: "",
|
||||
Share: []FileShare{},
|
||||
Sync: []FileSync{},
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Date(2019, 1, 1, 2, 6, 51, 0, time.UTC),
|
||||
CreatedIn: 2,
|
||||
UpdatedAt: time.Date(2020, 3, 28, 14, 6, 0, 0, time.UTC),
|
||||
|
@ -196,7 +192,6 @@ var FileFixtures = map[string]File{
|
|||
FileError: "Error",
|
||||
Share: []FileShare{},
|
||||
Sync: []FileSync{},
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Date(2018, 1, 1, 2, 6, 51, 0, time.UTC),
|
||||
CreatedIn: 2,
|
||||
UpdatedAt: time.Date(2029, 3, 28, 14, 6, 0, 0, time.UTC),
|
||||
|
@ -235,7 +230,6 @@ var FileFixtures = map[string]File{
|
|||
FileError: "",
|
||||
Share: []FileShare{},
|
||||
Sync: []FileSync{},
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Date(2018, 1, 1, 2, 6, 51, 0, time.UTC),
|
||||
CreatedIn: 2,
|
||||
UpdatedAt: time.Date(2029, 3, 28, 14, 6, 0, 0, time.UTC),
|
||||
|
@ -274,7 +268,6 @@ var FileFixtures = map[string]File{
|
|||
FileError: "",
|
||||
Share: []FileShare{},
|
||||
Sync: []FileSync{},
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Date(2018, 1, 1, 2, 6, 51, 0, time.UTC),
|
||||
CreatedIn: 2,
|
||||
UpdatedAt: time.Date(2029, 3, 28, 14, 6, 0, 0, time.UTC),
|
||||
|
@ -313,7 +306,6 @@ var FileFixtures = map[string]File{
|
|||
FileError: "",
|
||||
Share: []FileShare{},
|
||||
Sync: []FileSync{},
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Date(2018, 1, 1, 2, 6, 51, 0, time.UTC),
|
||||
CreatedIn: 2,
|
||||
UpdatedAt: time.Date(2029, 3, 28, 14, 6, 0, 0, time.UTC),
|
||||
|
@ -352,7 +344,6 @@ var FileFixtures = map[string]File{
|
|||
FileError: "",
|
||||
Share: []FileShare{},
|
||||
Sync: []FileSync{},
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Date(2018, 1, 1, 2, 6, 51, 0, time.UTC),
|
||||
CreatedIn: 2,
|
||||
UpdatedAt: time.Date(2029, 3, 28, 14, 6, 0, 0, time.UTC),
|
||||
|
@ -391,7 +382,6 @@ var FileFixtures = map[string]File{
|
|||
FileError: "Error",
|
||||
Share: []FileShare{},
|
||||
Sync: []FileSync{},
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Date(2018, 1, 1, 2, 6, 51, 0, time.UTC),
|
||||
CreatedIn: 2,
|
||||
UpdatedAt: time.Date(2029, 3, 28, 14, 6, 0, 0, time.UTC),
|
||||
|
@ -430,7 +420,6 @@ var FileFixtures = map[string]File{
|
|||
FileError: "",
|
||||
Share: []FileShare{},
|
||||
Sync: []FileSync{},
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Date(2019, 1, 1, 2, 6, 51, 0, time.UTC),
|
||||
CreatedIn: 2,
|
||||
UpdatedAt: time.Date(2020, 3, 28, 14, 6, 0, 0, time.UTC),
|
||||
|
@ -469,7 +458,6 @@ var FileFixtures = map[string]File{
|
|||
FileError: "",
|
||||
Share: []FileShare{},
|
||||
Sync: []FileSync{},
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Date(2019, 1, 1, 2, 6, 51, 0, time.UTC),
|
||||
CreatedIn: 2,
|
||||
UpdatedAt: time.Date(2020, 3, 28, 14, 6, 0, 0, time.UTC),
|
||||
|
@ -508,7 +496,6 @@ var FileFixtures = map[string]File{
|
|||
FileError: "",
|
||||
Share: []FileShare{},
|
||||
Sync: []FileSync{},
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Date(2019, 1, 1, 2, 6, 51, 0, time.UTC),
|
||||
CreatedIn: 2,
|
||||
UpdatedAt: time.Date(2020, 3, 28, 14, 6, 0, 0, time.UTC),
|
||||
|
|
|
@ -35,7 +35,6 @@ type Folder struct {
|
|||
FolderIgnore bool `json:"Ignore" yaml:"Ignore,omitempty"`
|
||||
FolderWatch bool `json:"Watch" yaml:"Watch,omitempty"`
|
||||
FileCount int `gorm:"-" json:"FileCount" yaml:"-"`
|
||||
Links []Link `gorm:"foreignkey:share_uid;association_foreignkey:folder_uid" json:"Links" yaml:"-"`
|
||||
CreatedAt time.Time `json:"-" yaml:"-"`
|
||||
UpdatedAt time.Time `json:"-" yaml:"-"`
|
||||
ModifiedAt *time.Time `json:"ModifiedAt,omitempty" yaml:"-"`
|
||||
|
|
|
@ -25,7 +25,6 @@ type Label struct {
|
|||
LabelDescription string `gorm:"type:text;" json:"Description" yaml:"Description,omitempty"`
|
||||
LabelNotes string `gorm:"type:text;" json:"Notes" yaml:"Notes,omitempty"`
|
||||
LabelCategories []*Label `gorm:"many2many:categories;association_jointable_foreignkey:category_id" json:"-" yaml:"-"`
|
||||
Links []Link `gorm:"foreignkey:share_uid;association_foreignkey:label_uid" json:"Links" yaml:"-"`
|
||||
PhotoCount int `gorm:"default:1" json:"PhotoCount" yaml:"-"`
|
||||
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
|
||||
|
@ -201,3 +200,8 @@ func (m *Label) UpdateClassify(label classify.Label) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Links returns all share links for this entity.
|
||||
func (m *Label) Links() Links {
|
||||
return FindLinks("", m.LabelUID)
|
||||
}
|
||||
|
|
|
@ -44,7 +44,6 @@ var LabelFixtures = LabelMap{
|
|||
LabelNotes: "",
|
||||
PhotoCount: 1,
|
||||
LabelCategories: []*Label{},
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
DeletedAt: nil,
|
||||
|
@ -62,7 +61,6 @@ var LabelFixtures = LabelMap{
|
|||
LabelNotes: "",
|
||||
PhotoCount: 2,
|
||||
LabelCategories: []*Label{},
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
DeletedAt: nil,
|
||||
|
@ -80,7 +78,6 @@ var LabelFixtures = LabelMap{
|
|||
LabelNotes: "",
|
||||
PhotoCount: 3,
|
||||
LabelCategories: []*Label{},
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
DeletedAt: nil,
|
||||
|
@ -98,7 +95,6 @@ var LabelFixtures = LabelMap{
|
|||
LabelNotes: "",
|
||||
PhotoCount: 4,
|
||||
LabelCategories: []*Label{},
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
DeletedAt: nil,
|
||||
|
@ -116,7 +112,6 @@ var LabelFixtures = LabelMap{
|
|||
LabelNotes: "",
|
||||
PhotoCount: 5,
|
||||
LabelCategories: []*Label{},
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
DeletedAt: nil,
|
||||
|
@ -134,7 +129,6 @@ var LabelFixtures = LabelMap{
|
|||
LabelNotes: "",
|
||||
PhotoCount: 1,
|
||||
LabelCategories: []*Label{},
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
DeletedAt: nil,
|
||||
|
@ -152,7 +146,6 @@ var LabelFixtures = LabelMap{
|
|||
LabelNotes: "",
|
||||
PhotoCount: 1,
|
||||
LabelCategories: []*Label{},
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
DeletedAt: nil,
|
||||
|
@ -170,7 +163,6 @@ var LabelFixtures = LabelMap{
|
|||
LabelNotes: "",
|
||||
PhotoCount: 1,
|
||||
LabelCategories: []*Label{},
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
DeletedAt: nil,
|
||||
|
@ -188,7 +180,6 @@ var LabelFixtures = LabelMap{
|
|||
LabelNotes: "",
|
||||
PhotoCount: 4,
|
||||
LabelCategories: []*Label{},
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
DeletedAt: nil,
|
||||
|
|
|
@ -1,42 +1,148 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
type Links []Link
|
||||
|
||||
// Link represents a sharing link.
|
||||
type Link struct {
|
||||
ShareUID string `gorm:"type:varbinary(42);primary_key;" json:"UID"`
|
||||
LinkToken string `gorm:"type:varbinary(255);primary_key;" json:"Token"`
|
||||
LinkPassword string `gorm:"type:varbinary(255);" json:"Password"`
|
||||
LinkExpires *time.Time `gorm:"type:datetime;" json:"Expires"`
|
||||
CanComment bool `json:"CanComment"`
|
||||
CanEdit bool `json:"CanEdit"`
|
||||
CreatedAt time.Time `deepcopier:"skip" json:"CreatedAt"`
|
||||
UpdatedAt time.Time `deepcopier:"skip" json:"UpdatedAt"`
|
||||
DeletedAt *time.Time `deepcopier:"skip" sql:"index" json:"DeletedAt,omitempty"`
|
||||
LinkUID string `gorm:"type:varbinary(42);primary_key;" json:"UID,omitempty" yaml:"UID,omitempty"`
|
||||
ShareUID string `gorm:"type:varbinary(42);unique_index:idx_links_uid_token;" json:"ShareUID"`
|
||||
ShareToken string `gorm:"type:varbinary(255);unique_index:idx_links_uid_token;" json:"ShareToken"`
|
||||
ShareExpires int `json:"ShareExpires"`
|
||||
HasPassword bool `json:"HasPassword"`
|
||||
CanComment bool `json:"CanComment"`
|
||||
CanEdit bool `json:"CanEdit"`
|
||||
CreatedAt time.Time `deepcopier:"skip" json:"CreatedAt"`
|
||||
UpdatedAt time.Time `deepcopier:"skip" json:"UpdatedAt"`
|
||||
}
|
||||
|
||||
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
|
||||
func (m *Link) BeforeCreate(scope *gorm.Scope) error {
|
||||
if err := scope.SetColumn("LinkToken", rnd.Token(10)); err != nil {
|
||||
if rnd.IsUID(m.LinkUID, 's') {
|
||||
return nil
|
||||
}
|
||||
|
||||
return scope.SetColumn("LinkUID", rnd.PPID('s'))
|
||||
}
|
||||
|
||||
// NewLink creates a sharing link.
|
||||
func NewLink(shareUID string, canComment, canEdit bool) Link {
|
||||
result := Link{
|
||||
LinkUID: rnd.PPID('s'),
|
||||
ShareUID: shareUID,
|
||||
ShareToken: rnd.Token(10),
|
||||
CanComment: canComment,
|
||||
CanEdit: canEdit,
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (m *Link) SetPassword(password string) error {
|
||||
pw := NewPassword(m.LinkUID, password)
|
||||
|
||||
if err := pw.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.HasPassword = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Link) InvalidPassword(password string) bool {
|
||||
if !m.HasPassword {
|
||||
return false
|
||||
}
|
||||
|
||||
pw := FindPassword(m.LinkUID)
|
||||
|
||||
if pw == nil {
|
||||
return password != ""
|
||||
}
|
||||
|
||||
return pw.InvalidPassword(password)
|
||||
}
|
||||
|
||||
// Create inserts a new row to the database.
|
||||
func (m *Link) Create() error {
|
||||
if !rnd.IsPPID(m.ShareUID, 0) {
|
||||
return fmt.Errorf("link: invalid share uid (%s)", m.ShareUID)
|
||||
}
|
||||
|
||||
if m.ShareToken == "" {
|
||||
return fmt.Errorf("link: empty share token")
|
||||
}
|
||||
|
||||
return Db().Create(m).Error
|
||||
}
|
||||
|
||||
// Save inserts a new row to the database or updates a row if the primary key already exists.
|
||||
func (m *Link) Save() error {
|
||||
if !rnd.IsPPID(m.ShareUID, 0) {
|
||||
return fmt.Errorf("link: invalid share uid (%s)", m.ShareUID)
|
||||
}
|
||||
|
||||
if m.ShareToken == "" {
|
||||
return fmt.Errorf("link: empty share token")
|
||||
}
|
||||
|
||||
return Db().Save(m).Error
|
||||
}
|
||||
|
||||
func (m *Link) Delete() error {
|
||||
if m.ShareToken == "" {
|
||||
return fmt.Errorf("link: empty share token")
|
||||
}
|
||||
|
||||
return Db().Delete(m).Error
|
||||
}
|
||||
|
||||
// FindLink returns an entity pointer if exists.
|
||||
func FindLink(linkUID string) *Link {
|
||||
result := Link{}
|
||||
|
||||
if err := Db().Where("link_uid = ?", linkUID).First(&result).Error; err == nil {
|
||||
return &result
|
||||
} else {
|
||||
log.Errorf("link: %s (not found)", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewLink creates a sharing link.
|
||||
func NewLink(password string, canComment, canEdit bool) Link {
|
||||
result := Link{
|
||||
LinkToken: rnd.Token(10),
|
||||
LinkPassword: password,
|
||||
CanComment: canComment,
|
||||
CanEdit: canEdit,
|
||||
// FindLinks returns a slice of links for a token and share UID (at least one must be provided).
|
||||
func FindLinks(shareToken, shareUID string) (result []Link) {
|
||||
if shareToken == "" && shareUID == "" {
|
||||
log.Errorf("link: share token and uid must not be empty at the same time (find links)")
|
||||
return []Link{}
|
||||
}
|
||||
|
||||
q := Db()
|
||||
|
||||
if shareToken != "" {
|
||||
q = q.Where("share_token = ?", shareToken)
|
||||
}
|
||||
|
||||
if shareUID != "" {
|
||||
q = q.Where("share_uid = ?", shareUID)
|
||||
}
|
||||
|
||||
if err := q.Find(&result).Error; err != nil {
|
||||
log.Errorf("link: %s (not found)", err)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// String returns an human readable identifier for logging.
|
||||
func (m *Link) String() string {
|
||||
return fmt.Sprintf("%s/%s", m.ShareUID, m.ShareToken)
|
||||
}
|
||||
|
|
|
@ -8,15 +8,13 @@ type LinkMap map[string]Link
|
|||
|
||||
var LinkFixtures = LinkMap{
|
||||
"1jxf3jfn2k": {
|
||||
LinkToken: "1jxf3jfn2k",
|
||||
LinkPassword: "somepassword",
|
||||
LinkExpires: &date,
|
||||
ShareUID: "4",
|
||||
ShareToken: "1jxf3jfn2k",
|
||||
ShareExpires: 0,
|
||||
ShareUID: "st9lxuqxpogaaba7",
|
||||
CanComment: true,
|
||||
CanEdit: false,
|
||||
CreatedAt: time.Date(2020, 3, 6, 2, 6, 51, 0, time.UTC),
|
||||
UpdatedAt: time.Date(2020, 3, 28, 14, 6, 0, 0, time.UTC),
|
||||
DeletedAt: nil,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -6,9 +6,10 @@ import (
|
|||
)
|
||||
|
||||
func TestNewLink(t *testing.T) {
|
||||
link := NewLink("passwd12", true, false)
|
||||
assert.Equal(t, "passwd12", link.LinkPassword)
|
||||
link := NewLink("st9lxuqxpogaaba1", true, false)
|
||||
assert.Equal(t, "st9lxuqxpogaaba1", link.ShareUID)
|
||||
assert.Equal(t, false, link.CanEdit)
|
||||
assert.Equal(t, true, link.CanComment)
|
||||
assert.Equal(t, 10, len(link.LinkToken))
|
||||
assert.Equal(t, 10, len(link.ShareToken))
|
||||
assert.Equal(t, 16, len(link.LinkUID))
|
||||
}
|
||||
|
|
82
internal/entity/password.go
Normal file
82
internal/entity/password.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// Password represents a password hash.
|
||||
type Password struct {
|
||||
UID string `gorm:"type:varbinary(255);primary_key;" json:"UID"`
|
||||
Hash string `deepcopier:"skip" gorm:"type:varbinary(255);" json:"Hash"`
|
||||
CreatedAt time.Time `deepcopier:"skip" json:"CreatedAt"`
|
||||
UpdatedAt time.Time `deepcopier:"skip" json:"UpdatedAt"`
|
||||
}
|
||||
|
||||
func NewPassword(uid, password string) Password {
|
||||
if uid == "" {
|
||||
panic("password: uid must not be empty")
|
||||
}
|
||||
|
||||
m := Password{UID: uid}
|
||||
|
||||
if password != "" {
|
||||
if err := m.SetPassword(password); err != nil {
|
||||
log.Errorf("password: %s (set password)", err)
|
||||
}
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Password) SetPassword(password string) error {
|
||||
if bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14); err != nil {
|
||||
return err
|
||||
} else {
|
||||
m.Hash = string(bytes)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Password) InvalidPassword(password string) bool {
|
||||
if m.Hash == "" && password == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
err := bcrypt.CompareHashAndPassword([]byte(m.Hash), []byte(password))
|
||||
return err != nil
|
||||
}
|
||||
|
||||
// Create inserts a new row to the database.
|
||||
func (m *Password) Create() error {
|
||||
return Db().Create(m).Error
|
||||
}
|
||||
|
||||
// Save inserts a new row to the database or updates a row if the primary key already exists.
|
||||
func (m *Password) Save() error {
|
||||
return Db().Save(m).Error
|
||||
}
|
||||
|
||||
// FindPassword returns an entity pointer if exists.
|
||||
func FindPassword(uid string) *Password {
|
||||
result := Password{}
|
||||
|
||||
if err := Db().Where("uid = ?", uid).First(&result).Error; err == nil {
|
||||
return &result
|
||||
} else {
|
||||
log.Errorf("password: %s (not found)", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns the password hash.
|
||||
func (m *Password) String() string {
|
||||
return m.Hash
|
||||
}
|
||||
|
||||
// Unknown returns true if the password is an empty string.
|
||||
func (m *Password) Unknown() bool {
|
||||
return m.Hash == ""
|
||||
}
|
|
@ -75,7 +75,6 @@ type Photo struct {
|
|||
Lens *Lens `gorm:"association_autoupdate:false;association_autocreate:false;association_save_reference:false" json:"Lens" yaml:"-"`
|
||||
Location *Location `gorm:"association_autoupdate:false;association_autocreate:false;association_save_reference:false" json:"Location" yaml:"-"`
|
||||
Place *Place `gorm:"association_autoupdate:false;association_autocreate:false;association_save_reference:false" json:"Place" yaml:"-"`
|
||||
Links []Link `gorm:"foreignkey:share_uid;association_foreignkey:photo_uid" json:"Links" yaml:"-"`
|
||||
Keywords []Keyword `json:"-" yaml:"-"`
|
||||
Albums []Album `json:"-" yaml:"-"`
|
||||
Files []File `yaml:"-"`
|
||||
|
@ -871,3 +870,8 @@ func (m *Photo) Approve() error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Links returns all share links for this entity.
|
||||
func (m *Photo) Links() Links {
|
||||
return FindLinks("", m.PhotoUID)
|
||||
}
|
||||
|
|
|
@ -65,7 +65,6 @@ var PhotoFixtures = PhotoMap{
|
|||
PlaceID: UnknownPlace.ID,
|
||||
Place: &UnknownPlace,
|
||||
PhotoCountry: UnknownPlace.CountryCode(),
|
||||
Links: []Link{},
|
||||
Keywords: []Keyword{
|
||||
KeywordFixtures.Get("bridge"),
|
||||
},
|
||||
|
@ -121,7 +120,6 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoMonth: 2,
|
||||
Details: DetailsFixtures.Get("lake", 1000001),
|
||||
DescriptionSrc: "",
|
||||
Links: []Link{},
|
||||
Keywords: []Keyword{},
|
||||
Albums: []Album{},
|
||||
Files: []File{},
|
||||
|
@ -171,7 +169,6 @@ var PhotoFixtures = PhotoMap{
|
|||
LocationID: UnknownLocation.ID,
|
||||
Place: &UnknownPlace,
|
||||
PlaceID: UnknownPlace.ID,
|
||||
Links: []Link{},
|
||||
Keywords: []Keyword{},
|
||||
Albums: []Album{},
|
||||
Files: []File{},
|
||||
|
@ -220,7 +217,6 @@ var PhotoFixtures = PhotoMap{
|
|||
CameraID: CameraFixtures.Pointer("canon-eos-6d").ID,
|
||||
Lens: LensFixtures.Pointer("lens-f-380"),
|
||||
LensID: LensFixtures.Pointer("lens-f-380").ID,
|
||||
Links: []Link{},
|
||||
Keywords: []Keyword{},
|
||||
Albums: []Album{},
|
||||
Files: []File{},
|
||||
|
@ -273,7 +269,6 @@ var PhotoFixtures = PhotoMap{
|
|||
CameraID: CameraFixtures.Pointer("canon-eos-6d").ID,
|
||||
Lens: LensFixtures.Pointer("lens-f-380"),
|
||||
LensID: LensFixtures.Pointer("lens-f-380").ID,
|
||||
Links: []Link{},
|
||||
Keywords: []Keyword{
|
||||
KeywordFixtures.Get("bridge"),
|
||||
},
|
||||
|
@ -326,7 +321,6 @@ var PhotoFixtures = PhotoMap{
|
|||
CameraID: CameraFixtures.Pointer("canon-eos-6d").ID,
|
||||
Lens: LensFixtures.Pointer("lens-f-380"),
|
||||
LensID: LensFixtures.Pointer("lens-f-380").ID,
|
||||
Links: []Link{},
|
||||
Keywords: []Keyword{},
|
||||
Albums: []Album{},
|
||||
Files: []File{},
|
||||
|
@ -375,7 +369,6 @@ var PhotoFixtures = PhotoMap{
|
|||
CameraID: CameraFixtures.Pointer("canon-eos-6d").ID,
|
||||
Lens: LensFixtures.Pointer("lens-f-380"),
|
||||
LensID: LensFixtures.Pointer("lens-f-380").ID,
|
||||
Links: []Link{},
|
||||
Keywords: []Keyword{},
|
||||
Albums: []Album{},
|
||||
Files: []File{},
|
||||
|
@ -424,7 +417,6 @@ var PhotoFixtures = PhotoMap{
|
|||
CameraID: CameraFixtures.Pointer("canon-eos-6d").ID,
|
||||
Lens: LensFixtures.Pointer("lens-f-380"),
|
||||
LensID: LensFixtures.Pointer("lens-f-380").ID,
|
||||
Links: []Link{},
|
||||
Keywords: []Keyword{},
|
||||
Albums: []Album{},
|
||||
Files: []File{},
|
||||
|
@ -473,7 +465,6 @@ var PhotoFixtures = PhotoMap{
|
|||
LocationSrc: "manual",
|
||||
Place: PlaceFixtures.Pointer("mexico"),
|
||||
PlaceID: PlaceFixtures.Pointer("mexico").ID,
|
||||
Links: []Link{},
|
||||
Keywords: []Keyword{},
|
||||
Albums: []Album{},
|
||||
Files: []File{},
|
||||
|
@ -522,7 +513,6 @@ var PhotoFixtures = PhotoMap{
|
|||
LocationSrc: "",
|
||||
Place: PlaceFixtures.Pointer("mexico"),
|
||||
PlaceID: PlaceFixtures.Pointer("mexico").ID,
|
||||
Links: []Link{},
|
||||
Keywords: []Keyword{},
|
||||
Albums: []Album{},
|
||||
Files: []File{},
|
||||
|
@ -571,7 +561,6 @@ var PhotoFixtures = PhotoMap{
|
|||
LocationSrc: "",
|
||||
Place: PlaceFixtures.Pointer("holidaypark"),
|
||||
PlaceID: PlaceFixtures.Pointer("holidaypark").ID,
|
||||
Links: []Link{},
|
||||
Keywords: []Keyword{},
|
||||
Albums: []Album{},
|
||||
Files: []File{},
|
||||
|
@ -620,7 +609,6 @@ var PhotoFixtures = PhotoMap{
|
|||
LocationSrc: "",
|
||||
Place: PlaceFixtures.Pointer("emptyNameLongCity"),
|
||||
PlaceID: PlaceFixtures.Pointer("emptyNameLongCity").ID,
|
||||
Links: []Link{},
|
||||
Keywords: []Keyword{},
|
||||
Albums: []Album{},
|
||||
Files: []File{},
|
||||
|
@ -669,7 +657,6 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoMonth: 7,
|
||||
Details: Details{},
|
||||
DescriptionSrc: "",
|
||||
Links: []Link{},
|
||||
Keywords: []Keyword{},
|
||||
Albums: []Album{},
|
||||
Files: []File{},
|
||||
|
@ -718,7 +705,6 @@ var PhotoFixtures = PhotoMap{
|
|||
CameraID: CameraFixtures.Pointer("canon-eos-6d").ID,
|
||||
Lens: LensFixtures.Pointer("lens-f-380"),
|
||||
LensID: LensFixtures.Pointer("lens-f-380").ID,
|
||||
Links: []Link{},
|
||||
Keywords: []Keyword{},
|
||||
Albums: []Album{},
|
||||
Files: []File{},
|
||||
|
@ -767,7 +753,6 @@ var PhotoFixtures = PhotoMap{
|
|||
LocationSrc: "",
|
||||
Place: PlaceFixtures.Pointer("mediumLongLocName"),
|
||||
PlaceID: PlaceFixtures.Pointer("mediumLongLocName").ID,
|
||||
Links: []Link{},
|
||||
Keywords: []Keyword{},
|
||||
Albums: []Album{},
|
||||
Files: []File{},
|
||||
|
@ -817,7 +802,6 @@ var PhotoFixtures = PhotoMap{
|
|||
CameraID: CameraFixtures.Pointer("canon-eos-6d").ID,
|
||||
Lens: LensFixtures.Pointer("lens-f-380"),
|
||||
LensID: LensFixtures.Pointer("lens-f-380").ID,
|
||||
Links: []Link{},
|
||||
Keywords: []Keyword{},
|
||||
Albums: []Album{},
|
||||
Files: []File{},
|
||||
|
@ -866,7 +850,6 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoMonth: 0,
|
||||
Details: DetailsFixtures.Get("lake", 1000015),
|
||||
DescriptionSrc: "location",
|
||||
Links: []Link{},
|
||||
Keywords: []Keyword{},
|
||||
Albums: []Album{},
|
||||
Files: []File{},
|
||||
|
@ -915,7 +898,6 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoMonth: 0,
|
||||
Details: DetailsFixtures.Get("lake", 1000015),
|
||||
DescriptionSrc: "location",
|
||||
Links: []Link{},
|
||||
Keywords: []Keyword{},
|
||||
Albums: []Album{},
|
||||
Files: []File{},
|
||||
|
@ -966,7 +948,6 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoMonth: 0,
|
||||
Details: DetailsFixtures.Get("lake", 1000015),
|
||||
DescriptionSrc: "location",
|
||||
Links: []Link{},
|
||||
Keywords: []Keyword{},
|
||||
Albums: []Album{},
|
||||
Files: []File{},
|
||||
|
@ -1017,7 +998,6 @@ var PhotoFixtures = PhotoMap{
|
|||
CameraID: CameraFixtures.Pointer("canon-eos-6d").ID,
|
||||
Lens: LensFixtures.Pointer("lens-f-380"),
|
||||
LensID: LensFixtures.Pointer("lens-f-380").ID,
|
||||
Links: []Link{},
|
||||
Keywords: []Keyword{},
|
||||
Albums: []Album{},
|
||||
Files: []File{},
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package form
|
||||
|
||||
// Link represents a sharing link form.
|
||||
// Link represents a link sharing form.
|
||||
type Link struct {
|
||||
Token string `json:"Token"`
|
||||
Password string `json:"Password"`
|
||||
Expires int `json:"Expires"`
|
||||
CanComment bool `json:"CanComment"`
|
||||
CanEdit bool `json:"CanEdit"`
|
||||
Password string `json:"Password"`
|
||||
ShareToken string `json:"ShareToken"`
|
||||
ShareExpires int `json:"ShareExpires"`
|
||||
CanComment bool `json:"CanComment"`
|
||||
CanEdit bool `json:"CanEdit"`
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ type AlbumResult struct {
|
|||
|
||||
// AlbumByUID returns a Album based on the UID.
|
||||
func AlbumByUID(albumUID string) (album entity.Album, err error) {
|
||||
if err := Db().Where("album_uid = ?", albumUID).Preload("Links").First(&album).Error; err != nil {
|
||||
if err := Db().Where("album_uid = ?", albumUID).First(&album).Error; err != nil {
|
||||
return album, err
|
||||
}
|
||||
|
||||
|
@ -108,7 +108,7 @@ func AlbumSearch(f form.AlbumSearch) (results []AlbumResult, err error) {
|
|||
s = s.Table("albums").
|
||||
Select(`albums.*,
|
||||
COUNT(photos_albums.album_uid) AS photo_count,
|
||||
COUNT(links.link_token) AS link_count`).
|
||||
COUNT(links.share_token) AS link_count`).
|
||||
Joins("LEFT JOIN photos_albums ON photos_albums.album_uid = albums.album_uid").
|
||||
Joins("LEFT JOIN links ON links.share_uid = albums.album_uid").
|
||||
Where("albums.deleted_at IS NULL").
|
||||
|
|
|
@ -55,7 +55,7 @@ func FilesByUID(u []string, limit int, offset int) (files entity.Files, err erro
|
|||
|
||||
// FileByPhotoUID
|
||||
func FileByPhotoUID(u string) (file entity.File, err error) {
|
||||
if err := Db().Where("photo_uid = ? AND file_primary = 1", u).Preload("Links").Preload("Photo").First(&file).Error; err != nil {
|
||||
if err := Db().Where("photo_uid = ? AND file_primary = 1", u).Preload("Photo").First(&file).Error; err != nil {
|
||||
return file, err
|
||||
}
|
||||
|
||||
|
@ -64,7 +64,7 @@ func FileByPhotoUID(u string) (file entity.File, err error) {
|
|||
|
||||
// VideoByPhotoUID
|
||||
func VideoByPhotoUID(u string) (file entity.File, err error) {
|
||||
if err := Db().Where("photo_uid = ? AND file_video = 1", u).Preload("Links").Preload("Photo").First(&file).Error; err != nil {
|
||||
if err := Db().Where("photo_uid = ? AND file_video = 1", u).Preload("Photo").First(&file).Error; err != nil {
|
||||
return file, err
|
||||
}
|
||||
|
||||
|
@ -73,7 +73,7 @@ func VideoByPhotoUID(u string) (file entity.File, err error) {
|
|||
|
||||
// FileByUID returns the file entity for a given UID.
|
||||
func FileByUID(uid string) (file entity.File, err error) {
|
||||
if err := Db().Where("file_uid = ?", uid).Preload("Links").Preload("Photo").First(&file).Error; err != nil {
|
||||
if err := Db().Where("file_uid = ?", uid).Preload("Photo").First(&file).Error; err != nil {
|
||||
return file, err
|
||||
}
|
||||
|
||||
|
@ -82,7 +82,7 @@ func FileByUID(uid string) (file entity.File, err error) {
|
|||
|
||||
// FileByHash finds a file with a given hash string.
|
||||
func FileByHash(fileHash string) (file entity.File, err error) {
|
||||
if err := Db().Where("file_hash = ?", fileHash).Preload("Links").Preload("Photo").First(&file).Error; err != nil {
|
||||
if err := Db().Where("file_hash = ?", fileHash).Preload("Photo").First(&file).Error; err != nil {
|
||||
return file, err
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ func PhotoLabel(photoID, labelID uint) (label entity.PhotoLabel, err error) {
|
|||
|
||||
// LabelBySlug returns a Label based on the slug name.
|
||||
func LabelBySlug(labelSlug string) (label entity.Label, err error) {
|
||||
if err := Db().Where("label_slug = ? OR custom_slug = ?", labelSlug, labelSlug).Preload("Links").First(&label).Error; err != nil {
|
||||
if err := Db().Where("label_slug = ? OR custom_slug = ?", labelSlug, labelSlug).First(&label).Error; err != nil {
|
||||
return label, err
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@ func LabelBySlug(labelSlug string) (label entity.Label, err error) {
|
|||
|
||||
// LabelByUID returns a Label based on the label UID.
|
||||
func LabelByUID(labelUID string) (label entity.Label, err error) {
|
||||
if err := Db().Where("label_uid = ?", labelUID).Preload("Links").First(&label).Error; err != nil {
|
||||
if err := Db().Where("label_uid = ?", labelUID).First(&label).Error; err != nil {
|
||||
return label, err
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ func PhotoByID(photoID uint64) (photo entity.Photo, err error) {
|
|||
return db.Order("photos_labels.uncertainty ASC, photos_labels.label_id DESC")
|
||||
}).
|
||||
Preload("Labels.Label").
|
||||
Preload("Links").
|
||||
Preload("Camera").
|
||||
Preload("Lens").
|
||||
Preload("Details").
|
||||
|
@ -35,7 +34,6 @@ func PhotoByUID(photoUID string) (photo entity.Photo, err error) {
|
|||
return db.Order("photos_labels.uncertainty ASC, photos_labels.label_id DESC")
|
||||
}).
|
||||
Preload("Labels.Label").
|
||||
Preload("Links").
|
||||
Preload("Camera").
|
||||
Preload("Lens").
|
||||
Preload("Details").
|
||||
|
@ -56,7 +54,6 @@ func PhotoPreloadByUID(photoUID string) (photo entity.Photo, err error) {
|
|||
return db.Order("photos_labels.uncertainty ASC, photos_labels.label_id DESC")
|
||||
}).
|
||||
Preload("Labels.Label").
|
||||
Preload("Links").
|
||||
Preload("Camera").
|
||||
Preload("Lens").
|
||||
Preload("Details").
|
||||
|
@ -100,7 +97,6 @@ func PhotosMaintenance(limit int, offset int) (entities entity.Photos, err error
|
|||
return db.Order("photos_labels.uncertainty ASC, photos_labels.label_id DESC")
|
||||
}).
|
||||
Preload("Labels.Label").
|
||||
Preload("Links").
|
||||
Preload("Camera").
|
||||
Preload("Lens").
|
||||
Preload("Details").
|
||||
|
|
|
@ -43,7 +43,10 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||
api.UpdatePhoto(v1, conf)
|
||||
api.GetPhotos(v1, conf)
|
||||
api.GetPhotoDownload(v1, conf)
|
||||
api.LinkPhoto(v1, conf)
|
||||
api.GetPhotoLinks(v1, conf)
|
||||
api.CreatePhotoLink(v1, conf)
|
||||
api.UpdatePhotoLink(v1, conf)
|
||||
api.DeletePhotoLink(v1, conf)
|
||||
api.ApprovePhoto(v1, conf)
|
||||
api.LikePhoto(v1, conf)
|
||||
api.DislikePhoto(v1, conf)
|
||||
|
@ -52,12 +55,14 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||
api.UpdatePhotoLabel(v1, conf)
|
||||
api.GetMomentsTime(v1, conf)
|
||||
api.GetFile(v1, conf)
|
||||
api.LinkFile(v1, conf)
|
||||
api.SetPhotoPrimary(v1, conf)
|
||||
|
||||
api.GetLabels(v1, conf)
|
||||
api.UpdateLabel(v1, conf)
|
||||
api.LinkLabel(v1, conf)
|
||||
api.GetLabelLinks(v1, conf)
|
||||
api.CreateLabelLink(v1, conf)
|
||||
api.UpdateLabelLink(v1, conf)
|
||||
api.DeleteLabelLink(v1, conf)
|
||||
api.LikeLabel(v1, conf)
|
||||
api.DislikeLabel(v1, conf)
|
||||
api.LabelThumbnail(v1, conf)
|
||||
|
@ -83,7 +88,10 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||
api.DeleteAlbum(v1, conf)
|
||||
api.DownloadAlbum(v1, conf)
|
||||
api.GetAlbums(v1, conf)
|
||||
api.LinkAlbum(v1, conf)
|
||||
api.GetAlbumLinks(v1, conf)
|
||||
api.CreateAlbumLink(v1, conf)
|
||||
api.UpdateAlbumLink(v1, conf)
|
||||
api.DeleteAlbumLink(v1, conf)
|
||||
api.LikeAlbum(v1, conf)
|
||||
api.DislikeAlbum(v1, conf)
|
||||
api.AlbumThumbnail(v1, conf)
|
||||
|
|
|
@ -21,7 +21,11 @@ func IsPPID(s string, prefix byte) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
return s[0] == prefix && IsLowerAlnum(s)
|
||||
if !IsLowerAlnum(s) {
|
||||
return false
|
||||
}
|
||||
|
||||
return prefix == 0 || s[0] == prefix
|
||||
}
|
||||
|
||||
// IsHex returns true if the string only contains hex numbers, dashes and letters without whitespace.
|
||||
|
|
Loading…
Reference in a new issue