feature (tag): support for tagging

This commit is contained in:
MickaelK
2025-08-07 23:05:44 +10:00
parent cc031163d9
commit 8ff5b47f06
13 changed files with 367 additions and 74 deletions

View File

@ -32,7 +32,7 @@ export default function(ctrl) {
const $main = qs($page, `[data-bind="filemanager-children"]`);
$main.classList.remove("hidden");
ctrl(createRender($main));
ctrlSidebar(createRender(qs($page, `[data-bind="sidebar"]`)));
ctrlSidebar(createRender(qs($page, `[data-bind="sidebar"]`)), {});
onDestroy(async() => {
if ((history.state.previous || "").startsWith("/view/") && location.pathname.startsWith("/files/")) {
await animate($main, { time: 100, keyframes: slideYOut(20), fill: "none" });

View File

@ -17,6 +17,21 @@
direction: rtl;
box-sizing: border-box;
}
.component_filemanager_shell .component_sidebar .component_skeleton {
margin-bottom: 5px;
width: calc(100% - 5px);
margin-left: 5px;
opacity: 0;
animation-duration: 0.5s;
animation-delay: 1s;
animation-name: skeleton-appear;
animation-fill-mode: forwards;
animation-iteration-count: 1;
}
@keyframes skeleton-appear {
from { opacity: 0; }
to { opacity: 1; }
}
.component_filemanager_shell .component_sidebar > div { direction: ltr; }
.component_filemanager_shell .component_sidebar h3 {
display: flex;
@ -79,7 +94,8 @@ body.touch-no .component_filemanager_shell .component_sidebar h3 img:hover {
width: 100%;
box-sizing: border-box;
}
.component_filemanager_shell .component_sidebar a:hover, .component_filemanager_shell .component_sidebar a[aria-selected="true"] {
.component_filemanager_shell .component_sidebar [data-bind="your-files"] a:hover,
.component_filemanager_shell .component_sidebar a[aria-selected="true"] {
background: var(--border);
border-radius: 3px;
}
@ -107,3 +123,26 @@ body.touch-no .component_filemanager_shell .component_sidebar h3 img:hover {
.component_filemanager_shell li.pointer {
cursor: pointer;
}
.component_filemanager_shell .component_sidebar [data-bind="taglist"] a {
margin-bottom: 2px;
justify-content: space-between;
font-size: 0.95rem;
}
.component_filemanager_shell .component_sidebar [data-bind="taglist"] a[aria-selected="true"] {
background: var(--light);
color: #f2f2f2;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
}
.component_filemanager_shell .component_sidebar [data-bind="taglist"] a[aria-selected="true"] svg {
display: block;
}
.component_filemanager_shell .component_sidebar [data-bind="taglist"] a svg {
display: none;
background: rgba(255, 255, 255, 0.03);
align-self: center;
width: 13px;
height: 13px;
border-radius: 50%;
padding: 4px;
}

View File

@ -1,16 +1,20 @@
import { createElement, createRender, onDestroy } from "../lib/skeleton/index.js";
import rxjs, { effect, onClick } from "../lib/rx.js";
import ajax from "../lib/ajax.js";
import assert from "../lib/assert.js";
import { fromHref, toHref } from "../lib/skeleton/router.js";
import { qs, qsa, safe } from "../lib/dom.js";
import { forwardURLParams } from "../lib/path.js";
import { settingsGet, settingsSave } from "../lib/store.js";
import { get as getConfig } from "../model/config.js";
import { loadCSS } from "../helpers/loader.js";
import t from "../locales/index.js";
import cache from "../pages/filespage/cache.js";
import { hooks, mv as mv$ } from "../pages/filespage/model_files.js";
import { extractPath, isDir, isNativeFileUpload } from "../pages/filespage/helper.js";
import { mv as mvVL, withVirtualLayer } from "../pages/filespage/model_virtual_layer.js";
import { getCurrentPath } from "../pages/viewerpage/common.js";
import { generateSkeleton } from "./skeleton.js";
const state = { scrollTop: 0, $cache: null };
const mv = (from, to) => withVirtualLayer(
@ -18,7 +22,7 @@ const mv = (from, to) => withVirtualLayer(
mvVL(from, to),
);
export default async function ctrlSidebar(render, nRestart = 0) {
export default async function ctrlSidebar(render, { nRestart = 0 }) {
if (!shouldDisplay()) return;
const $sidebar = render(createElement(`
@ -28,11 +32,6 @@ export default async function ctrlSidebar(render, nRestart = 0) {
<input type="text" placeholder="${t("Your Files")}" />
</h3>
<div data-bind="your-files"></div>
<h3>
<img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHZlcnNpb249IjEuMSIgd2lkdGg9IjE2IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KICAgIDxwYXRoIHN0eWxlPSJmaWxsOiAjNTc1OTVhOyIgZD0iTTEgNy43NzVWMi43NUMxIDEuNzg0IDEuNzg0IDEgMi43NSAxaDUuMDI1Yy40NjQgMCAuOTEuMTg0IDEuMjM4LjUxM2w2LjI1IDYuMjVhMS43NSAxLjc1IDAgMCAxIDAgMi40NzRsLTUuMDI2IDUuMDI2YTEuNzUgMS43NSAwIDAgMS0yLjQ3NCAwbC02LjI1LTYuMjVBMS43NTIgMS43NTIgMCAwIDEgMSA3Ljc3NVptMS41IDBjMCAuMDY2LjAyNi4xMy4wNzMuMTc3bDYuMjUgNi4yNWEuMjUuMjUgMCAwIDAgLjM1NCAwbDUuMDI1LTUuMDI1YS4yNS4yNSAwIDAgMCAwLS4zNTRsLTYuMjUtNi4yNWEuMjUuMjUgMCAwIDAtLjE3Ny0uMDczSDIuNzVhLjI1LjI1IDAgMCAwLS4yNS4yNVpNNiA1YTEgMSAwIDEgMSAwIDIgMSAxIDAgMCAxIDAtMloiPjwvcGF0aD4NCjwvc3ZnPg0K" alt="tag">
${t("Tags")}
</h3>
<div data-bind="your-tags"></div>
</div>
`));
@ -76,7 +75,12 @@ export default async function ctrlSidebar(render, nRestart = 0) {
ctrlNavigationPane(render, { $sidebar, nRestart });
// feature: tag viewer
effect(rxjs.merge(
rxjs.of(null),
rxjs.fromEvent(window, "filestash::tag"),
).pipe(rxjs.tap(() => {
ctrlTagPane(createRender(qs($sidebar, `[data-bind="your-tags"]`)));
})));
}
const withResize = (function() {
@ -254,37 +258,63 @@ async function _createListOfFiles(path, currentName, dirpath) {
}
async function ctrlTagPane(render) {
if (!getConfig("enable_tags", false)) return;
render(createElement(`<div>${generateSkeleton(2)}</div>`));
const $page = createElement(`
<div>
<h3>
<img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHZlcnNpb249IjEuMSIgd2lkdGg9IjE2IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KICAgIDxwYXRoIHN0eWxlPSJmaWxsOiAjNTc1OTVhOyIgZD0iTTEgNy43NzVWMi43NUMxIDEuNzg0IDEuNzg0IDEgMi43NSAxaDUuMDI1Yy40NjQgMCAuOTEuMTg0IDEuMjM4LjUxM2w2LjI1IDYuMjVhMS43NSAxLjc1IDAgMCAxIDAgMi40NzRsLTUuMDI2IDUuMDI2YTEuNzUgMS43NSAwIDAgMS0yLjQ3NCAwbC02LjI1LTYuMjVBMS43NTIgMS43NTIgMCAwIDEgMSA3Ljc3NVptMS41IDBjMCAuMDY2LjAyNi4xMy4wNzMuMTc3bDYuMjUgNi4yNWEuMjUuMjUgMCAwIDAgLjM1NCAwbDUuMDI1LTUuMDI1YS4yNS4yNSAwIDAgMCAwLS4zNTRsLTYuMjUtNi4yNWEuMjUuMjUgMCAwIDAtLjE3Ny0uMDczSDIuNzVhLjI1LjI1IDAgMCAwLS4yNS4yNVpNNiA1YTEgMSAwIDEgMSAwIDIgMSAxIDAgMCAxIDAtMloiPjwvcGF0aD4NCjwvc3ZnPg0K" alt="tag">
${t("Tags")}
</h3>
<ul>
<li data-bind="taglist"></li>
</ul>
</div>
`);
render($page);
// only enable this pane in canary mode until it's actually ready
if (new URLSearchParams(location.search).get("canary") !== "true") {
$page.classList.add("hidden");
const orFail = (something) => assert.type(something, HTMLElement);
orFail(orFail($page.parentElement).previousElementSibling).classList.add("hidden");
const tags = await ajax({
url: forwardURLParams(`api/metadata/search`, ["share"]),
method: "POST",
responseType: "json",
body: JSON.stringify({
"tags": [],
"path": getCurrentPath("(/view/|/files/)"),
}),
}).pipe(
rxjs.map(({ responseJSON }) =>
responseJSON.results
.filter(({type}) => type === "folder")
.map(({ name }) => name)
.sort()
),
rxjs.catchError(() => rxjs.of([])),
).toPromise();
if (tags.length === 0) {
render(createElement("<div></div>"));
return;
}
render($page);
const tags = [
{ name: t("Bookmark"), color: "green" },
{ name: "important", color: "red" },
{ name: "foobar", color: "saddlebrown" },
];
const $tmpl = (name, color) => createElement(`
<a data-link href="/tags/${name}/?canary=true" draggable="false">
<svg class="component_icon" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="50" style="opacity: 0.25; fill: ${color};" />
</svg>
const $fragment = document.createDocumentFragment();
tags.forEach((name) => {
const $tag = createElement(`
<a data-link draggable="false" class="no-select">
<div class="ellipsis">${name}</div>
<svg class="component_icon" draggable="false" alt="close" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</a>
`);
const $fragment = document.createDocumentFragment();
tags.forEach(({ name, color }) => {
$fragment.appendChild($tmpl(name, color));
const url = new URL(location.href);
if (url.searchParams.getAll("tag").indexOf(name) === -1) {
$tag.setAttribute("href", forwardURLParams(getCurrentPath() + "?tag=" + name, ["share", "tag"]));
} else {
url.searchParams.delete("tag", name);
$tag.setAttribute("href", url.toString());
$tag.setAttribute("aria-selected", "true");
}
$fragment.appendChild($tag);
});
qs($page, `[data-bind="taglist"]`).appendChild($fragment);
}

View File

@ -12,10 +12,11 @@ export function join(baseURL, segment) {
}
export function forwardURLParams(url, allowed = []) {
const _url = new URL(window.location.origin + "/" + url);
const link = new URL(window.location.origin + "/" + url);
for (const [key, value] of new URLSearchParams(location.search)) {
if (allowed.indexOf(key) < 0) continue;
_url.searchParams.set(key, value);
else if (link.searchParams.getAll(key).indexOf(value) !== -1) continue;
link.searchParams.append(key, value);
}
return _url.pathname.substring(1) + _url.search;
return link.pathname.substring(1) + link.search;
}

View File

@ -119,7 +119,7 @@ function componentLeft(render, { $scroll, getSelectionLength$ }) {
<button data-action="share" title="${t("Share")}" class="${(getConfig("enable_share") && !new URLSearchParams(location.search).has("share")) ? "" : "hidden"}">
${t("Share")}
</button>
<button data-action="tag" title="${t("Tag")}" class="${new URLSearchParams(location.search).get("canary") === "true" ? "" : "hidden"}" tabindex="-1">
<button data-action="tag" title="${t("Tag")}" class="${getConfig("enable_tags", false) ? "" : "hidden"}">
${t("Tag")}
</button>
`))),

View File

@ -1,14 +1,10 @@
.component_tag {
font-size: 1rem;
}
.component_tag [data-bind="taglist"] {
display: flex;
gap: 5px;
flex-wrap: wrap;
margin: -10px;
padding: 20px 10px 10px 10px;
.component_tag form {
margin-bottom: 15px;
}
.component_tag input {
.component_tag form input {
font-size: 1rem;
border: 2px solid var(--border);
border-radius: 5px;
@ -18,9 +14,20 @@
display: block;
box-sizing: border-box;
}
.component_tag input::placeholder {
.component_tag form input::placeholder {
color: rgba(0,0,0,0.2);
}
.component_tag [data-bind="taglist"] {
display: flex;
gap: 5px;
flex-wrap: wrap;
margin: -3px 0 0px 0px;
padding: 0;
}
.component_tag [data-bind="taglist"] .component_skeleton {
margin: 0;
height: 29px;
}
.component_tag .tag {
display: flex;
background: #f2f2f2;
@ -31,6 +38,7 @@
font-size: 0.95rem;
cursor: pointer;
letter-spacing: -0.5px;
text-transform: lowercase;
}
.component_tag .tag.active {
background: var(--dark);

View File

@ -1,40 +1,147 @@
import { createElement } from "../../lib/skeleton/index.js";
import rxjs, { effect, onClick } from "../../lib/rx.js";
import { forwardURLParams } from "../../lib/path.js";
import ajax from "../../lib/ajax.js";
import { qs } from "../../lib/dom.js";
import assert from "../../lib/assert.js";
import { generateSkeleton } from "../../components/skeleton.js";
import t from "../../locales/index.js";
const shareID = new URLSearchParams(location.search).get("share");
const $tmpl = createElement(`
<div class="tag no-select">
<div class="ellipsis">Projects</div>
<svg class="component_icon" draggable="false" alt="close" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg style="${shareID ? "opacity:0.2" : ""}" class="component_icon" draggable="false" alt="close" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</div>
`);
export default function(render, { path }) {
const tags = [
{ name: "Bookmark", active: true },
{ name: "Projects" },
{ name: "Important" },
];
export default async function(render, { path }) {
const $modal = createElement(`
<div class="component_tag">
<form>
<input type="text" placeholder="${t("Create a Tag")}" value="">
<form class="${shareID ? "hidden" : ""}">
<input name="tag" type="text" placeholder="${t("Add a Tag")}" value="">
</form>
<div class="scroll-y" data-bind="taglist"></div>
<div class="scroll-y" data-bind="taglist">
${generateSkeleton(1)}
</div>
</div>
`);
const $fragment = document.createDocumentFragment();
tags.forEach(({ name, active }) => {
render($modal);
const tags$ = new rxjs.BehaviorSubject(await rxjs.zip(
ajax({ url: forwardURLParams(`api/metadata?path=${path}`, ["share"]), method: "GET", responseType: "json" }).pipe(
rxjs.map(({ responseJSON }) =>
responseJSON.results
.reduce((acc, { id, value }) => {
if (id !== "tags") return acc;
acc = acc.concat(value.split(", ").map(
(name) => ({ name, active: true })
));
return acc;
}, [])
),
),
ajax({ url: forwardURLParams("api/metadata/search", ["share"]), method: "POST", responseType: "json", body: { path: "/" }}).pipe(
rxjs.map(({ responseJSON }) =>
responseJSON.results
.filter(({ type, name }) => type === "folder")
.map(({ name }) => ({ name, active: false }))
),
),
).pipe(rxjs.map(([currentTags, allTags]) => {
for (let i=0; i<allTags.length; i++) {
for (let j=0; j<currentTags.length; j++) {
if (currentTags[j].name === allTags[i].name) {
allTags[i].active = true;
break;
}
}
}
if (!shareID && allTags.length === 0) {
return [{ name: t("bookmark"), active: false }];
}
if (shareID) return allTags.filter(({ active }) => active);
return allTags;
})).toPromise());
const save = (tags) => ajax({
url: forwardURLParams(`api/metadata?path=${path}`, ["share"]),
method: "POST",
body: tags.length === 0 ? [] : [{
id: "tags",
type: "hidden",
value: tags.join(", "),
}],
}).pipe(rxjs.tap(() => window.dispatchEvent(new Event("filestash::tag"))));
// feature: create DOM
const dom$ = tags$.pipe(
rxjs.map((tags) => tags.sort((a, b) => a.name > b.name ? 1 : -1)),
rxjs.map((tags) => tags.map(({ name, active }) => {
const $el = assert.type($tmpl, HTMLElement).cloneNode(true);
$el.firstElementChild.innerText = name;
if (active) $el.classList.add("active");
$fragment.appendChild($el);
});
qs($modal, `[data-bind="taglist"]`).appendChild($fragment);
render($modal, ({ id }) => { console.log(`QUIT id=${id} path=${path}`); });
qs($el, "svg").onclick = (e) => {
if (shareID) return;
e.preventDefault();
e.stopPropagation();
$el.classList.remove("active");
tags$.next(tags$.value.filter((tag) => {
return tag.name !== $el.innerText.trim();
}));
save(tags$.value
.filter(({ active }) => active)
.map(({ name }) => name)).toPromise();
}
return $el;
})),
rxjs.tap(($nodes) => {
const $container = qs($modal, `[data-bind="taglist"]`);
if ($nodes.length === 0) $container.replaceChildren(createElement(`<div class="center full-width">∅</div>`));
else $container.replaceChildren(...$nodes);
}),
);
// feature: tag creation
effect(rxjs.fromEvent(qs($modal, "form"), "submit").pipe(
rxjs.filter(() => !shareID),
rxjs.tap((e) => {
e.preventDefault();
const tagname = new FormData(e.target).get("tag").toLowerCase().trim();
if (!tagname) return;
else if (tags$.value.find(({ name }) => name === tagname)) return;
qs($modal, `input[name="tag"]`).value = "";
tags$.next(tags$.value.concat({
name: tagname,
active: true,
}));
}),
rxjs.mergeMap(() => save(
tags$.value
.filter(({ active }) => !!active)
.map(({ name }) => name)
)),
));
// feature: toggle tags
effect(dom$.pipe(
rxjs.mergeMap(($nodes) => rxjs.merge(
...$nodes.map(($node) => onClick($node)),
).pipe(
rxjs.filter(() => !shareID),
rxjs.tap(($node) => $node.classList.toggle("active")),
rxjs.debounceTime(800),
rxjs.map(() =>
$nodes
.filter(($node) => $node.classList.contains("active"))
.map(($node) => $node.innerText.trim())
.filter((text) => !!text)
),
)),
rxjs.mergeMap((tags) => save(tags)),
));
}

View File

@ -14,7 +14,7 @@ export function getDownloadUrl() {
return forwardURLParams("api/files/cat?path=" + encodeURIComponent(getCurrentPath()), ["share"]);
}
export function getCurrentPath() {
export function getCurrentPath(start = "/view/") {
const fullpath = fromHref(location.pathname + location.hash);
return decodeURIComponent(fullpath.replace(new RegExp("^/view"), ""));
return decodeURIComponent(fullpath.replace(new RegExp("^" + start), "/"));
}

View File

@ -358,7 +358,6 @@ func (this *Configuration) Export() interface{} {
Name string `json:"name"`
UploadButton bool `json:"upload_button"`
Connections interface{} `json:"connections"`
EnableShare bool `json:"enable_share"`
SharedLinkDefaultAccess string `json:"share_default_access"`
SharedLinkRedirect string `json:"share_redirect"`
Logout string `json:"logout"`
@ -370,9 +369,11 @@ func (this *Configuration) Export() interface{} {
FilePageDefaultView string `json:"default_view"`
AuthMiddleware []string `json:"auth"`
Thumbnailer []string `json:"thumbnailer"`
EnableChromecast bool `json:"enable_chromecast"`
Origin string `json:"origin"`
Version string `json:"version"`
EnableChromecast bool `json:"enable_chromecast"`
EnableShare bool `json:"enable_share"`
EnableTags bool `json:"enable_tags"`
}{
Editor: this.Get("general.editor").String(),
ForkButton: this.Get("general.fork_button").Bool(),
@ -380,7 +381,6 @@ func (this *Configuration) Export() interface{} {
Name: this.Get("general.name").String(),
UploadButton: this.Get("general.upload_button").Bool(),
Connections: this.Conn,
EnableShare: this.Get("features.share.enable").Bool(),
SharedLinkDefaultAccess: this.Get("features.share.default_access").String(),
SharedLinkRedirect: this.Get("features.share.redirect").String(),
Logout: this.Get("general.logout").String(),
@ -408,7 +408,6 @@ func (this *Configuration) Export() interface{} {
}
return tArray
}(),
EnableChromecast: this.Get("features.protection.enable_chromecast").Bool(),
Origin: func() string {
host := this.Get("general.host").String()
if host == "" {
@ -421,6 +420,11 @@ func (this *Configuration) Export() interface{} {
return scheme + host
}(),
Version: BUILD_REF,
EnableChromecast: this.Get("features.protection.enable_chromecast").Bool(),
EnableShare: this.Get("features.share.enable").Bool(),
EnableTags: func() bool {
return Hooks.Get.Metadata() != nil
}(),
}
}

View File

@ -254,6 +254,16 @@ func (this Get) StaticPatch() [][]byte {
return staticOverrides
}
var meta IMetadata
func (this Register) Metadata(m IMetadata) {
meta = m
}
func (this Get) Metadata() IMetadata {
return meta
}
func init() {
Hooks.Register.FrontendOverrides(OverrideVideoSourceMapper)
}

View File

@ -66,6 +66,18 @@ type AuditQueryResult struct {
RenderHTML string `json:"render"`
}
const (
MetaModeTag = 1 << iota
MetaModeBookmark
MetaModeForm
)
type IMetadata interface {
Get(ctx *App, path string) ([]FormElement, error)
Set(ctx *App, path string, value []FormElement) error
Search(ctx *App, basePath string, facets map[string]any) ([]IFile, error)
}
type File struct {
FName string `json:"name"`
FType string `json:"type"`

76
server/ctrl/metadata.go Normal file
View File

@ -0,0 +1,76 @@
package ctrl
import (
"encoding/json"
"fmt"
"net/http"
. "github.com/mickael-kerjean/filestash/server/common"
)
func MetaGet(ctx *App, w http.ResponseWriter, r *http.Request) {
m := Hooks.Get.Metadata()
if m == nil {
SendErrorResult(w, ErrNotImplemented)
return
}
path, err := PathBuilder(ctx, r.URL.Query().Get("path"))
if err != nil {
SendErrorResult(w, err)
return
}
out, err := m.Get(ctx, path)
if err != nil {
SendErrorResult(w, err)
return
}
SendSuccessResults(w, out)
}
func MetaUpsert(ctx *App, w http.ResponseWriter, r *http.Request) {
m := Hooks.Get.Metadata()
if m == nil {
SendErrorResult(w, ErrNotImplemented)
return
}
path, err := PathBuilder(ctx, r.URL.Query().Get("path"))
if err != nil {
SendErrorResult(w, err)
return
}
forms := []FormElement{}
if err := json.NewDecoder(r.Body).Decode(&forms); err != nil {
SendErrorResult(w, ErrNotImplemented)
return
}
if err := m.Set(ctx, path, forms); err != nil {
SendErrorResult(w, err)
return
}
SendSuccessResult(w, nil)
}
func MetaSearch(ctx *App, w http.ResponseWriter, r *http.Request) {
m := Hooks.Get.Metadata()
if m == nil {
SendErrorResult(w, ErrNotImplemented)
return
}
facets := map[string]any{}
if err := json.NewDecoder(r.Body).Decode(&facets); err != nil {
SendErrorResult(w, ErrNotImplemented)
return
}
path, err := PathBuilder(ctx, fmt.Sprintf("%s", facets["path"]))
if err != nil {
SendErrorResult(w, err)
return
}
delete(facets, "path")
out, err := m.Search(ctx, path, facets)
if err != nil {
SendErrorResult(w, err)
return
}
SendSuccessResults(w, out)
}

View File

@ -73,6 +73,12 @@ func Build(r *mux.Router, a App) {
middlewares = []Middleware{ApiHeaders, SecureHeaders, SecureOrigin, BodyParser, CanManageShare, PluginInjector}
share.HandleFunc("/{share}", NewMiddlewareChain(ShareUpsert, middlewares, a)).Methods("POST")
meta := r.PathPrefix(WithBase("/api/metadata")).Subrouter()
middlewares = []Middleware{ApiHeaders, SecureHeaders, SecureOrigin, WithPublicAPI, SessionStart, LoggedInOnly, PluginInjector}
meta.HandleFunc("", NewMiddlewareChain(MetaGet, middlewares, a)).Methods("GET")
meta.HandleFunc("", NewMiddlewareChain(MetaUpsert, middlewares, a)).Methods("POST")
meta.HandleFunc("/search", NewMiddlewareChain(MetaSearch, middlewares, a)).Methods("POST")
// Webdav server / Shared Link
middlewares = []Middleware{IndexHeaders, SecureHeaders, PluginInjector}
r.HandleFunc(WithBase("/s/{share}"), NewMiddlewareChain(ServeFrontofficeHandler, middlewares, a)).Methods("GET")