mirror of
https://github.com/mickael-kerjean/filestash
synced 2025-12-06 08:22:24 +01:00
feature (wip): wip for tags
This commit is contained in:
parent
c4e5da9169
commit
dbf0878335
10 changed files with 297 additions and 71 deletions
4
client/assets/icons/tag.svg
Normal file
4
client/assets/icons/tag.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 129 129" style="fill:#6f6f6f;stroke:#6f6f6f;">
|
||||
<path d="m 51.741297,117.22165 c -2.466177,0 -4.788482,-0.96594 -6.535367,-2.69223 L 13.885528,83.208986 c -1.726339,-1.746857 -2.692249,-4.069156 -2.692249,-6.535348 0,-2.466185 0.96591,-4.788492 2.692249,-6.535368 L 66.764417,17.259402 c 3.000527,-3.000523 8.446629,-5.261167 12.700782,-5.261167 h 27.703341 c 5.09681,0 9.24816,4.151389 9.24816,9.227587 v 27.703353 c 0,4.233618 -2.26065,9.700278 -5.26116,12.700812 L 58.276658,114.50887 c -1.746869,1.74684 -4.04863,2.71278 -6.535361,2.71278 z M 79.465199,18.163668 c -2.548374,0 -6.535359,1.644105 -8.32334,3.452635 L 18.262982,74.495181 c -0.575448,0.575452 -0.883718,1.356397 -0.883718,2.178457 0,0.8426 0.328824,1.603006 0.883718,2.178453 l 31.320416,31.320439 c 0.575435,0.5754 1.356403,0.88368 2.178452,0.88368 0.842611,0 1.603015,-0.32882 2.178449,-0.88368 L 106.8192,57.293642 c 1.80851,-1.808537 3.45264,-5.774997 3.45264,-8.343889 V 21.246394 c 0,-1.705772 -1.37695,-3.082726 -3.08273,-3.082726 z" style="stroke-width:2.2;" />
|
||||
<circle cx="142.1252" cy="101.61908" r="28.324646" style="fill-opacity:0;stroke-width:20;" transform="matrix(0.38692388,0,0,0.38692388,30.309631,2.968202)" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -65,6 +65,8 @@ export const Icon = (props) => {
|
|||
img = "/assets/icons/delete.svg";
|
||||
} else if (props.name === "share") {
|
||||
img = "/assets/icons/share.svg";
|
||||
} else if (props.name === "tag") {
|
||||
img = "/assets/icons/tag.svg";
|
||||
} else if (props.name === "bucket") {
|
||||
img = img_bucket;
|
||||
} else if (props.name === "download_white") {
|
||||
|
|
|
|||
|
|
@ -1,56 +1,61 @@
|
|||
import { cache, currentShare, currentBackend } from "../helpers/";
|
||||
|
||||
class TagManager {
|
||||
all(tagPath = "/", maxSize = -1) {
|
||||
all(tagPath = "/") {
|
||||
return cache.get(cache.FILE_TAG, [currentBackend(), currentShare()]).then((DB) => {
|
||||
if (DB === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (tagPath == "/") {
|
||||
const scoreFn = (acc, el) => (acc + el.replace(/[^\/]/g, "").length);
|
||||
const tags = Object.keys(DB.tags).sort((a, b) => {
|
||||
if (DB.tags[a].length === DB.tags[b].length) {
|
||||
return DB.tags[a].reduce(scoreFn, 0) - DB.tags[b].reduce(scoreFn, 0);
|
||||
}
|
||||
return DB.tags[a].length < DB.tags[b].length ? 1 : -1;
|
||||
});
|
||||
if(tags.length === 0) {
|
||||
return ["Bookmark"];
|
||||
} else if(tags.length >= 5) {
|
||||
return ["All"].concat(tags.slice(0, 5));
|
||||
}
|
||||
return tags;
|
||||
const tags = this._tagPathStringToArray(tagPath);
|
||||
if (tags.length === 0) {
|
||||
return Object.keys(DB.tags);
|
||||
}
|
||||
return [
|
||||
// "Bookmark", "wiki", "B", "C", "D", "E", "F"
|
||||
];
|
||||
|
||||
// STEP1: build the graph of selected tags
|
||||
|
||||
// STEP2: build the node that connects to the initial graph
|
||||
return Object.keys(DB.tags)
|
||||
.map((tag) => {
|
||||
if (tags.indexOf(tag) !== -1) { // ignore tag that are already selected
|
||||
return { tag, scrore: 0 };
|
||||
}
|
||||
return {
|
||||
tag,
|
||||
score: DB.tags[tag].reduce((path, acc) => {
|
||||
// TODO
|
||||
return acc;
|
||||
}, 0),
|
||||
}
|
||||
})
|
||||
.filter((t) => t && t.score > 0)
|
||||
.sort((a, b) => a.score > b.score)
|
||||
.map((d) => d.tag);
|
||||
});
|
||||
}
|
||||
|
||||
files(tagPath) {
|
||||
const tags = this._tagPathStringToArray(tagPath, false);
|
||||
if (tags.length === 0) return Promise.resolve([]);
|
||||
else if (tags.length > 1) return Promise.resolve([]); // TODO
|
||||
let tags = this._tagPathStringToArray(tagPath);
|
||||
|
||||
return cache.get(cache.FILE_TAG, [currentBackend(), currentShare()]).then((DB) => {
|
||||
if(!DB) return [];
|
||||
switch(tags[0]) {
|
||||
case "All":
|
||||
return this.all()
|
||||
.then((tags) => (tags.reduce((acc, el) => {
|
||||
return DB.tags[el] ? acc.concat(DB.tags[el]) : acc;
|
||||
}, [])));
|
||||
default:
|
||||
return Promise.resolve(DB.tags[tags[0]] || []);
|
||||
if (!DB) return [];
|
||||
else if (!DB.tags) return [];
|
||||
else if (tags.length === 0) tags = Object.keys(DB.tags);
|
||||
|
||||
// push all the candidates in an array
|
||||
let paths = (DB.tags[tags[0]] || []).map((t) => ({path: t, tag: tags[0]}));
|
||||
for (let i=1; i<tags.length; i++) {
|
||||
const tp = DB.tags[tags[i]];
|
||||
if (!tp) continue;
|
||||
paths = paths.concat(tp.map((t) => ({path: t, tag: tags[i]})));
|
||||
}
|
||||
|
||||
// mark element of the array that shouldn't be here
|
||||
return paths;
|
||||
});
|
||||
}
|
||||
|
||||
_tagPathStringToArray(tagPathString, removeFirst = true) {
|
||||
return tagPathString
|
||||
.split("/")
|
||||
.filter((r) => r !== "" && (removeFirst ? r !== "All" : true));
|
||||
_tagPathStringToArray(tagPathString) {
|
||||
return tagPathString.split("/").filter((r) => r !== "");
|
||||
}
|
||||
|
||||
addTagToFile(tag, path) {
|
||||
|
|
@ -72,13 +77,15 @@ class TagManager {
|
|||
DB.tags[tag].splice(idx, 1);
|
||||
if (DB.tags[tag].length === 0) {
|
||||
delete DB.tags[tag];
|
||||
delete DB.weight[tag];
|
||||
}
|
||||
return DB;
|
||||
});
|
||||
}
|
||||
|
||||
import(DB) {
|
||||
if(JSON.stringify(Object.keys(DB)) !== JSON.stringify(["tags", "share", "backend"])) {
|
||||
return Promise.reject(new Error("Not Valid"));
|
||||
}
|
||||
return cache.upsert(cache.FILE_TAG, [currentBackend(), currentShare()], () => {
|
||||
return DB;
|
||||
});
|
||||
|
|
@ -89,7 +96,7 @@ class TagManager {
|
|||
return cache.get(cache.FILE_TAG, key)
|
||||
.then((a) => {
|
||||
if (a === null) {
|
||||
return {tags: {}, weight: {}, share: key[1], backend: key[0]}
|
||||
return {tags: {}, share: key[1], backend: key[0]}
|
||||
}
|
||||
return a;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@ export function FrequentlyAccess({ files, tags }) {
|
|||
<NgIf cond={!!tags && tags.length > 0}>
|
||||
<span className="caption">{t("Tag")}</span>
|
||||
<div className="frequent_wrapper">
|
||||
<Link to={"/tags/"}>
|
||||
<Icon name={"directory"} />
|
||||
<div>All</div>
|
||||
</Link>
|
||||
{
|
||||
tags && tags.map((tag, index) => {
|
||||
return (
|
||||
|
|
|
|||
139
client/pages/filespage/tag.js
Normal file
139
client/pages/filespage/tag.js
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import React, { useState, useRef, useEffect } from "react";
|
||||
|
||||
import {
|
||||
Icon, Input,
|
||||
} from "../../components/";
|
||||
import { Tags } from "../../model/";
|
||||
import { t } from "../../locales/";
|
||||
import "./tag.scss";
|
||||
|
||||
export function TagComponent({ path }) {
|
||||
const [DB, setDB] = useState(null);
|
||||
const [input, setInput] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
Tags.export().then((db) => {
|
||||
setDB(db);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onFormSubmit = (e) => {
|
||||
if (!DB) return;
|
||||
else if(!DB.tags) return;
|
||||
|
||||
e.preventDefault();
|
||||
const it = input.trim().toLowerCase();
|
||||
if (it === "") return;
|
||||
|
||||
const newDB = {...DB};
|
||||
newDB.tags = {};
|
||||
newDB.tags[it] = [path];
|
||||
Object.keys(DB.tags).forEach((tag) => {
|
||||
newDB.tags[tag] = DB.tags[tag];
|
||||
});
|
||||
setDB(newDB);
|
||||
setInput("");
|
||||
Tags.import(newDB);
|
||||
};
|
||||
const onClickTag = (tagName) => {
|
||||
if (!DB) return;
|
||||
else if(!DB.tags) return;
|
||||
console.log("CLICK ON ", tagName, "idx", DB.tags[tagName].indexOf(path));
|
||||
|
||||
const newDB = {...DB};
|
||||
if (isTagActive(tagName)) {
|
||||
newDB.tags[tagName].splice(DB.tags[tagName].indexOf(path), 1);
|
||||
} else {
|
||||
newDB.tags[tagName].push(path);
|
||||
}
|
||||
setDB(newDB);
|
||||
Tags.import(newDB);
|
||||
};
|
||||
const onClickMoveUp = (tagName) => {
|
||||
if (!DB) return;
|
||||
else if(!DB.tags) return;
|
||||
|
||||
const newDB = {...DB};
|
||||
const keys = Object.keys(DB.tags) || [];
|
||||
const n = keys.indexOf(tagName);
|
||||
if (n === 0) return;
|
||||
|
||||
newDB.tags = {};
|
||||
for (let i=0; i<keys.length; i++) {
|
||||
if (i === n-1) {
|
||||
newDB.tags[keys[i+1]] = DB.tags[keys[i+1]];
|
||||
} else if (i === n) {
|
||||
newDB.tags[keys[i-1]] = DB.tags[keys[i-1]];
|
||||
} else {
|
||||
newDB.tags[keys[i]] = DB.tags[keys[i]];
|
||||
}
|
||||
}
|
||||
setDB(newDB);
|
||||
Tags.import(newDB);
|
||||
};
|
||||
const onClickMoveDown = (tagName) => {
|
||||
if (!DB) return;
|
||||
else if(!DB.tags) return;
|
||||
|
||||
const newDB = {...DB};
|
||||
const keys = Object.keys(DB.tags) || [];
|
||||
const n = keys.indexOf(tagName);
|
||||
if (n === keys.length - 1) return;
|
||||
|
||||
newDB.tags = {};
|
||||
for (let i=0; i<keys.length; i++) {
|
||||
if (i === n) {
|
||||
newDB.tags[keys[i+1]] = DB.tags[keys[i+1]];
|
||||
} else if (i === n+1) {
|
||||
newDB.tags[keys[i-1]] = DB.tags[keys[i-1]];
|
||||
} else {
|
||||
newDB.tags[keys[i]] = DB.tags[keys[i]];
|
||||
}
|
||||
}
|
||||
setDB(newDB);
|
||||
Tags.import(newDB);
|
||||
};
|
||||
const onClickRemove = (tagName) => {
|
||||
const newDB = {...DB};
|
||||
delete newDB.tags[tagName];
|
||||
Tags.import(newDB);
|
||||
setDB(newDB);
|
||||
};
|
||||
|
||||
const isTagActive = (tagName) => {
|
||||
if (!DB) return false;
|
||||
else if(!DB.tags) return false;
|
||||
return DB.tags[tagName].indexOf(path) !== -1;
|
||||
}
|
||||
|
||||
const TAGS = DB && Object.keys(DB.tags);
|
||||
return (
|
||||
<div className="component_tag">
|
||||
<form onSubmit={(e) => onFormSubmit(e)}>
|
||||
<Input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder={t("Create a Tag")}
|
||||
autoFocus />
|
||||
</form>
|
||||
<div className="scroll-y">
|
||||
{
|
||||
TAGS && TAGS.length > 0 ?
|
||||
Object.keys(DB.tags).map((tag) => (
|
||||
<div key={tag} className={"box no-select" + (isTagActive(tag) ? " active" : "")}>
|
||||
<div onClick={() => onClickTag(tag)}>{ tag } <span className="count">{ (DB.tags[tag] || []).length }</span></div>
|
||||
<Icon name="arrow_top" onClick={() => onClickMoveUp(tag)} />
|
||||
<Icon name="arrow_bottom" onClick={() => onClickMoveDown(tag)} />
|
||||
<Icon name="close" onClick={() => onClickRemove(tag)} />
|
||||
</div>
|
||||
)) : (
|
||||
<div className={"box no-select"}>
|
||||
<div onClick={() => onClickTag(t("Bookmark"))}>{ t("Bookmark") }</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
43
client/pages/filespage/tag.scss
Normal file
43
client/pages/filespage/tag.scss
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
.component_tag {
|
||||
input {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
input::placeholder {
|
||||
font-weight: 100;
|
||||
}
|
||||
.box {
|
||||
display: flex;
|
||||
background: var(--bg-color);
|
||||
transition: background 0.1s ease;
|
||||
&.active{ background: var(--primary); color: var(--color); }
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
|
||||
> div {
|
||||
flex-grow: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
img {
|
||||
width: 20px;
|
||||
background: rgba(0,0,0,0.05);
|
||||
border-radius: 50%;
|
||||
margin-left: 5px;
|
||||
&[alt="close"] {
|
||||
width: 14px;
|
||||
padding: 3px;
|
||||
}
|
||||
}
|
||||
.count {
|
||||
opacity: 0.5;
|
||||
font-size: 0.9rem;
|
||||
&:before { content: "["; }
|
||||
&:after { content: "]"; }
|
||||
}
|
||||
}
|
||||
.scroll-y {
|
||||
overflow-y: auto !important;
|
||||
max-height: 200px;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import { Card, NgIf, Icon, EventEmitter, img_placeholder, Input } from "../../co
|
|||
import { pathBuilder, basename, filetype, prompt, alert, leftPad, getMimeType, debounce, memory } from "../../helpers/";
|
||||
import { Files } from "../../model/";
|
||||
import { ShareComponent } from "./share";
|
||||
import { TagComponent } from "./tag";
|
||||
import { t } from "../../locales/";
|
||||
|
||||
|
||||
|
|
@ -227,6 +228,13 @@ class ExistingThingComponent extends React.Component {
|
|||
this.setState({ delete_request: false });
|
||||
}
|
||||
|
||||
onTagRequest() {
|
||||
alert.now(
|
||||
<TagComponent path={this.props.file.path} type={this.props.file.type} />,
|
||||
() => {},
|
||||
)
|
||||
}
|
||||
|
||||
onShareRequest(filename) {
|
||||
alert.now(
|
||||
<ShareComponent path={this.props.file.path} type={this.props.file.type} />,
|
||||
|
|
@ -321,6 +329,7 @@ class ExistingThingComponent extends React.Component {
|
|||
onClickRename={this.onRenameRequest.bind(this)}
|
||||
onClickDelete={this.onDeleteRequest.bind(this)}
|
||||
onClickShare={this.onShareRequest.bind(this)}
|
||||
onClickTag={this.onTagRequest.bind(this)}
|
||||
is_renaming={this.state.is_renaming}
|
||||
can_rename={this.props.metadata.can_rename !== false}
|
||||
can_delete={this.props.metadata.can_delete !== false}
|
||||
|
|
@ -371,6 +380,7 @@ class Filename extends React.Component {
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.props.onRename(this.state.filename);
|
||||
return false;
|
||||
}
|
||||
|
||||
onCancel() {
|
||||
|
|
@ -380,6 +390,7 @@ class Filename extends React.Component {
|
|||
|
||||
preventSelect(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
@ -412,7 +423,7 @@ class Filename extends React.Component {
|
|||
<NgIf cond={this.props.is_renaming === true} type="inline">
|
||||
<form
|
||||
onClick={this.preventSelect}
|
||||
onSubmit={this.onRename.bind(this)}>
|
||||
onSubmit={(e) => { e.preventDefault(); e.stopPropagation(); this.onRename(e) }}>
|
||||
<input
|
||||
value={this.state.filename}
|
||||
onChange={(e) => this.setState({ filename: e.target.value })}
|
||||
|
|
@ -443,6 +454,11 @@ const ActionButton = (props) => {
|
|||
props.onClickShare();
|
||||
};
|
||||
|
||||
const onTag = (e) => {
|
||||
e.preventDefault();
|
||||
props.onClickTag();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="component_action">
|
||||
<NgIf
|
||||
|
|
@ -453,14 +469,25 @@ const ActionButton = (props) => {
|
|||
onClick={onRename}
|
||||
className="component_updater--icon" />
|
||||
</NgIf>
|
||||
<NgIf
|
||||
type="inline"
|
||||
cond={props.can_delete !== false}>
|
||||
<Icon
|
||||
name="delete"
|
||||
onClick={onDelete}
|
||||
className="component_updater--icon" />
|
||||
</NgIf>
|
||||
{
|
||||
/canary/.test(location.search) ? (
|
||||
<span type="inline">
|
||||
<Icon
|
||||
name="tag"
|
||||
onClick={onTag}
|
||||
className="component_updater--icon" />
|
||||
</span>
|
||||
) : (
|
||||
<NgIf
|
||||
type="inline"
|
||||
cond={props.can_delete !== false}>
|
||||
<Icon
|
||||
name="delete"
|
||||
onClick={onDelete}
|
||||
className="component_updater--icon" />
|
||||
</NgIf>
|
||||
)
|
||||
}
|
||||
<NgIf
|
||||
type="inline"
|
||||
cond={props.can_share !== false}>
|
||||
|
|
|
|||
|
|
@ -39,21 +39,11 @@ export function TagsPageComponent({ match }) {
|
|||
}
|
||||
|
||||
const onClickRemoveFile = (file) => {
|
||||
prompt.now(
|
||||
t("Confirm by typing") + ": remove",
|
||||
(answer) => {
|
||||
if (answer !== "remove") {
|
||||
return Promise.resolve();
|
||||
}
|
||||
Tags.removeTagFromFile(
|
||||
path.split("/").filter((r) => !!r).slice(-1)[0],
|
||||
file,
|
||||
);
|
||||
setRefresh(refresh + 1);
|
||||
return Promise.resolve();
|
||||
},
|
||||
() => {},
|
||||
Tags.removeTagFromFile(
|
||||
file.tag,
|
||||
file.path,
|
||||
);
|
||||
setRefresh(refresh + 1);
|
||||
}
|
||||
|
||||
const onClickMoreDropdown = (what) => {
|
||||
|
|
@ -74,15 +64,12 @@ export function TagsPageComponent({ match }) {
|
|||
notify.send(t("Not Valid"), "error");
|
||||
return;
|
||||
}
|
||||
if(JSON.stringify(Object.keys(jsonObject)) !== JSON.stringify(["tags", "weight", "share", "backend"])) {
|
||||
notify.send(t("Not Valid"), "error");
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
Tags.import(jsonObject).then(() => {
|
||||
setLoading(false);
|
||||
setRefresh(refresh + 1);
|
||||
}).catch((err) => {
|
||||
notify.send(t(err && err.message), "error");
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
|
@ -111,7 +98,11 @@ export function TagsPageComponent({ match }) {
|
|||
<div className="component_container">
|
||||
<h1>
|
||||
{
|
||||
path.split("/").filter((r) => r).map((tag, idx) => (
|
||||
path === "/" ? (
|
||||
<Link to="/">
|
||||
<Icon name="arrow_left" />home
|
||||
</Link>
|
||||
) : path.split("/").filter((r) => r).map((tag, idx) => (
|
||||
<React.Fragment key={idx}>#{tag} </React.Fragment>
|
||||
))
|
||||
}
|
||||
|
|
@ -173,15 +164,16 @@ export function TagsPageComponent({ match }) {
|
|||
{
|
||||
files && files.map((file, idx) => (
|
||||
<div className="component_thing view-list" key={idx}>
|
||||
<Link to={(isAFolder(file) ? URL_FILES : URL_VIEWER) + file}>
|
||||
<Link to={(isAFolder(file.path) ? URL_FILES : URL_VIEWER) + file.path}>
|
||||
<Card>
|
||||
<span className="component_action" style={{float: "right"}} onClick={(e) => { e.preventDefault(); onClickRemoveFile(file)}}>
|
||||
<Icon name="close" />
|
||||
</span>
|
||||
<span><Icon name={filetype(file)} /></span>
|
||||
<span><Icon name={filetype(file.path)} /></span>
|
||||
<span className="component_filename">
|
||||
<span className="file-details">
|
||||
{basename(file)} <br/><i>{file}</i>
|
||||
{basename(file.path)}<br/>
|
||||
{path === "/" && (<i>#{file.tag} </i>)}<i>{file.path}</i>
|
||||
</span>
|
||||
</span>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -18,9 +18,15 @@
|
|||
h1 {
|
||||
font-weight: 100;
|
||||
text-transform: uppercase;
|
||||
margin: -7px 0 0 0;
|
||||
margin: 0 0 0 0;
|
||||
float: left;
|
||||
line-height: 30px;
|
||||
font-size: 1.7rem;
|
||||
line-height: 1.7rem;
|
||||
a .component_icon {
|
||||
float: left;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
& > .component_container {
|
||||
|
|
|
|||
|
|
@ -161,8 +161,10 @@ func CanManageShare(fn func(*App, http.ResponseWriter, *http.Request)) func(ctx
|
|||
fn(ctx, res, req)
|
||||
return
|
||||
}
|
||||
Log.Debug("middleware::session::share 'permission denied - s.CanShare[%+v] s.Backend[%s]'", s.CanShare, s.Backend)
|
||||
} else {
|
||||
Log.Debug("middleware::session::share 'permission denied - s.CanShare[%+v] s.Backend[%s] GenerateID[%s]'", s.CanShare, s.Backend, id)
|
||||
}
|
||||
Log.Debug("middleware::session::share 'permission denied - s.CanShare[%+v] s.Backend[%s] GenerateID[%s]'", s.CanShare, s.Backend, id)
|
||||
SendErrorResult(res, ErrPermissionDenied)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue