mirror of
https://github.com/filebrowser/filebrowser.git
synced 2026-07-17 16:36:49 +00:00
fix: Fix conflict modal and add a resume transfert option (#5884)
Co-authored-by: Henrique Dias <mail@hacdias.com>
This commit is contained in:
parent
1f22fe65ec
commit
f4e148523e
15 changed files with 311 additions and 72 deletions
|
|
@ -49,6 +49,12 @@ export async function fetch(url: string, signal?: AbortSignal) {
|
|||
return data;
|
||||
}
|
||||
|
||||
export async function fetchAll(url: string): Promise<RecursiveEntry[]> {
|
||||
url = removePrefix(url);
|
||||
const res = await fetchURL(`/api/resources/recursive${url}`, {});
|
||||
return (await res.json()) as RecursiveEntry[];
|
||||
}
|
||||
|
||||
async function resourceAction(url: string, method: ApiMethod, content?: any) {
|
||||
url = removePrefix(url);
|
||||
|
||||
|
|
|
|||
|
|
@ -191,7 +191,6 @@ const drop = async (event: Event) => {
|
|||
return;
|
||||
}
|
||||
const path = el.__vue__.url;
|
||||
const baseItems = (await api.fetch(path)).items;
|
||||
|
||||
const action = (overwrite?: boolean, rename?: boolean) => {
|
||||
const action =
|
||||
|
|
@ -205,7 +204,7 @@ const drop = async (event: Event) => {
|
|||
.catch($showError);
|
||||
};
|
||||
|
||||
const conflict = upload.checkConflict(items, baseItems);
|
||||
const conflict = await upload.checkConflict(items, path);
|
||||
|
||||
if (conflict.length > 0) {
|
||||
layoutStore.showHover({
|
||||
|
|
|
|||
|
|
@ -122,8 +122,7 @@ export default {
|
|||
});
|
||||
};
|
||||
|
||||
const dstItems = (await api.fetch(this.dest)).items;
|
||||
const conflict = upload.checkConflict(items, dstItems);
|
||||
const conflict = upload.checkConflict(items, this.dest);
|
||||
|
||||
if (conflict.length > 0) {
|
||||
this.showHover({
|
||||
|
|
|
|||
|
|
@ -122,8 +122,7 @@ export default {
|
|||
});
|
||||
};
|
||||
|
||||
const dstItems = (await api.fetch(this.dest)).items;
|
||||
const conflict = upload.checkConflict(items, dstItems);
|
||||
const conflict = upload.checkConflict(items, this.dest);
|
||||
|
||||
if (conflict.length > 0) {
|
||||
this.showHover({
|
||||
|
|
|
|||
|
|
@ -112,6 +112,16 @@
|
|||
<i class="material-icons">undo</i>
|
||||
{{ $t("buttons.skipAll") }}
|
||||
</button>
|
||||
<button @click="(e) => resume(e)">
|
||||
<i class="material-icons">replay</i>
|
||||
{{ $t("buttons.resumeTransfer") }}
|
||||
<span class="info-tooltip" @click.stop="() => {}">
|
||||
<i class="material-icons info-icon">info_outline</i>
|
||||
<span class="info-tooltip-text">
|
||||
{{ $t("buttons.resumeTransferTooltip") }}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button @click="personalized = true">
|
||||
<i class="material-icons">checklist</i>
|
||||
{{ $t("buttons.singleDecision") }}
|
||||
|
|
@ -194,6 +204,17 @@ const humanTime = (modified: string | number | undefined) => {
|
|||
return dayjs(modified).format("L LT");
|
||||
};
|
||||
|
||||
const resume = (event: Event) => {
|
||||
conflict.value.forEach((item) => {
|
||||
if (item.isSmallerOnServer) {
|
||||
item.checked = ["origin"];
|
||||
} else {
|
||||
item.checked = ["dest"];
|
||||
}
|
||||
});
|
||||
currentPrompt?.confirm(event, conflict.value);
|
||||
};
|
||||
|
||||
const resolve = (event: Event, result: Array<"origin" | "dest">) => {
|
||||
for (const item of conflict.value) {
|
||||
item.checked = result;
|
||||
|
|
@ -304,4 +325,42 @@ const toogleCheckAll = (e: Event) => {
|
|||
.result-buttons > button:hover {
|
||||
border: solid 1px var(--icon-blue);
|
||||
}
|
||||
|
||||
.info-tooltip {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
font-size: 1rem;
|
||||
color: var(--icon-blue);
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.info-tooltip-text {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: var(--surfacePrimary, #333);
|
||||
color: var(--textPrimary, #fff);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.3;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
width: 220px;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
transition: opacity 200ms ease-in-out;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.info-tooltip:hover .info-tooltip-text {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@
|
|||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
import * as upload from "@/utils/upload";
|
||||
|
|
@ -43,11 +42,10 @@ import * as upload from "@/utils/upload";
|
|||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
|
||||
const fileStore = useFileStore();
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
// TODO: this is a copy of the same function in FileListing.vue
|
||||
const uploadInput = (event: Event) => {
|
||||
const uploadInput = async (event: Event) => {
|
||||
const files = (event.currentTarget as HTMLInputElement)?.files;
|
||||
if (files === null) return;
|
||||
|
||||
|
|
@ -67,7 +65,8 @@ const uploadInput = (event: Event) => {
|
|||
}
|
||||
|
||||
const path = route.path.endsWith("/") ? route.path : route.path + "/";
|
||||
const conflict = upload.checkConflict(uploadFiles, fileStore.req!.items);
|
||||
|
||||
const conflict = await upload.checkConflict(uploadFiles, path);
|
||||
|
||||
if (conflict.length > 0) {
|
||||
layoutStore.showHover({
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@
|
|||
"rename": "Rename",
|
||||
"replace": "Replace",
|
||||
"reportIssue": "Report Issue",
|
||||
"resumeTransfer": "Resume previous transfert",
|
||||
"resumeTransferTooltip": "Skip all conflicting files, except the ones that are smaller on the server as we suppose that their transfert has been interrupted.",
|
||||
"save": "Save",
|
||||
"schedule": "Schedule",
|
||||
"search": "Search",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@
|
|||
"rename": "Renommer",
|
||||
"replace": "Remplacer",
|
||||
"reportIssue": "Signaler un problème",
|
||||
"resumeTransfer": "Reprendre un transfert précedement interrompu",
|
||||
"resumeTransferTooltip": "Ignorer tous les fichiers en conflit, sauf ceux qui sont plus petits sur le serveur, car nous supposons que leur transfert a été interrompu.",
|
||||
"save": "Enregistrer",
|
||||
"schedule": "Planifier",
|
||||
"search": "Rechercher",
|
||||
|
|
|
|||
13
frontend/src/types/file.d.ts
vendored
13
frontend/src/types/file.d.ts
vendored
|
|
@ -71,10 +71,19 @@ interface ConflictingResource {
|
|||
name: string;
|
||||
origin: ConflictingItem;
|
||||
dest: ConflictingItem;
|
||||
checked: Array<"origin" | "dest">;
|
||||
checked: Array<"origin" | "dest", "origin-resume">;
|
||||
isSmallerOnServer?: boolean;
|
||||
}
|
||||
|
||||
|
||||
interface CsvData {
|
||||
headers: string[];
|
||||
rows: string[][];
|
||||
}
|
||||
|
||||
interface RecursiveEntry {
|
||||
path: string;
|
||||
name: string;
|
||||
size: number;
|
||||
modified: string;
|
||||
isDir: boolean;
|
||||
}
|
||||
|
|
|
|||
1
frontend/src/types/upload.d.ts
vendored
1
frontend/src/types/upload.d.ts
vendored
|
|
@ -16,6 +16,7 @@ interface UploadEntry {
|
|||
size: number;
|
||||
isDir: boolean;
|
||||
fullPath?: string;
|
||||
to?: string;
|
||||
file?: File;
|
||||
overwrite?: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,7 @@ import { describe, it, expect } from "vitest";
|
|||
|
||||
type Entry = { name: string; isFile: boolean; isDirectory: boolean };
|
||||
|
||||
function simulateScanFiles(
|
||||
dirName: string,
|
||||
entryBatches: Entry[][]
|
||||
): string[] {
|
||||
function simulateScanFiles(dirName: string, entryBatches: Entry[][]): string[] {
|
||||
const paths: string[] = [];
|
||||
let batchIndex = 0;
|
||||
|
||||
|
|
@ -28,7 +25,8 @@ function simulateScanFiles(
|
|||
|
||||
// Mirrors readReaderContent() from upload.ts lines 118-138
|
||||
function readReaderContent(directory: string): void {
|
||||
const entries = batchIndex < entryBatches.length ? entryBatches[batchIndex] : [];
|
||||
const entries =
|
||||
batchIndex < entryBatches.length ? entryBatches[batchIndex] : [];
|
||||
batchIndex++;
|
||||
|
||||
if (entries.length > 0) {
|
||||
|
|
@ -56,9 +54,7 @@ describe("scanFiles path construction", () => {
|
|||
{ name: "file1.xlsx", isFile: true, isDirectory: false },
|
||||
{ name: "file2.xlsx", isFile: true, isDirectory: false },
|
||||
],
|
||||
[
|
||||
{ name: "file3.xlsx", isFile: true, isDirectory: false },
|
||||
],
|
||||
[{ name: "file3.xlsx", isFile: true, isDirectory: false }],
|
||||
]);
|
||||
|
||||
expect(paths).toHaveLength(3);
|
||||
|
|
@ -70,9 +66,7 @@ describe("scanFiles path construction", () => {
|
|||
|
||||
it("single batch should work fine (no regression)", () => {
|
||||
const paths = simulateScanFiles("TestFolder", [
|
||||
[
|
||||
{ name: "file1.xlsx", isFile: true, isDirectory: false },
|
||||
],
|
||||
[{ name: "file1.xlsx", isFile: true, isDirectory: false }],
|
||||
]);
|
||||
|
||||
expect(paths).toEqual(["TestFolder/file1.xlsx"]);
|
||||
|
|
|
|||
|
|
@ -1,60 +1,164 @@
|
|||
import { useLayoutStore } from "@/stores/layout";
|
||||
import { useUploadStore } from "@/stores/upload";
|
||||
import url from "@/utils/url";
|
||||
import { files as api } from "@/api";
|
||||
import { removePrefix } from "@/api/utils";
|
||||
|
||||
export function checkConflict(
|
||||
files: UploadList | Array<any>,
|
||||
dest: ResourceItem[]
|
||||
): ConflictingResource[] {
|
||||
if (typeof dest === "undefined" || dest === null) {
|
||||
dest = [];
|
||||
}
|
||||
const conflictingFiles: ConflictingResource[] = [];
|
||||
interface UploadEntryWithChild extends UploadEntry {
|
||||
children?: UploadEntryWithChild[];
|
||||
originalIndex: number;
|
||||
}
|
||||
|
||||
const folder_upload = files[0].fullPath !== undefined;
|
||||
/**
|
||||
* Convert UploadList into a forest (array of root nodes).
|
||||
* This properly handles uploads with multiple top-level files/folders
|
||||
* instead of assuming a single root.
|
||||
* @param flatArray
|
||||
* @param basePath
|
||||
*/
|
||||
function flatToForest(
|
||||
flatArray: UploadList,
|
||||
basePath: string
|
||||
): UploadEntryWithChild[] {
|
||||
const nodeMap: Record<string, UploadEntryWithChild> = {};
|
||||
|
||||
function getFile(name: string): ResourceItem | null {
|
||||
for (const item of dest) {
|
||||
if (item.name == name) return item;
|
||||
}
|
||||
// First pass: create all nodes
|
||||
flatArray.forEach((item, index) => {
|
||||
// File list is created from very different action and info available are not always the same.
|
||||
// By doing a drag and drop or upload a folder (both from the browser or from the OS) we have the fullPath property available
|
||||
// By uploading a single file using the file input, we only have the "name" property
|
||||
// By doing drag and drop from filebrowser to filebrowser, we have the "to" property available but not the fullPath
|
||||
const fullPathOrTo =
|
||||
item.fullPath || item.to?.replace(basePath, "") || item.name;
|
||||
nodeMap[fullPathOrTo!] = {
|
||||
fullPath: fullPathOrTo,
|
||||
isDir: item.isDir,
|
||||
name: item.name,
|
||||
size: item.size,
|
||||
originalIndex: index,
|
||||
...(item.isDir && { children: [] }),
|
||||
...(item.file && { file: item.file }),
|
||||
};
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
const roots: UploadEntryWithChild[] = [];
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
const name = file.name;
|
||||
// Second pass: build hierarchy
|
||||
flatArray.forEach((item) => {
|
||||
// see comment before to explanation
|
||||
const fullPathOrTo =
|
||||
item.fullPath || item.to?.replace(basePath, "") || item.name;
|
||||
|
||||
if (folder_upload && file.isDir) {
|
||||
const dirs = file.fullPath?.split("/");
|
||||
// For folder uploads, destination listing is flat and only contains
|
||||
// top-level entries. Treating every nested file as a conflict when the
|
||||
// parent folder exists blocks the whole upload (see #5798), so skip
|
||||
// preflight conflict detection for nested files.
|
||||
if (dirs && dirs.length > 1) {
|
||||
continue;
|
||||
const node = nodeMap[fullPathOrTo!];
|
||||
const lastSlash = fullPathOrTo!.lastIndexOf("/");
|
||||
|
||||
if (lastSlash === -1) {
|
||||
roots.push(node);
|
||||
} else {
|
||||
const parentPath = fullPathOrTo!.substring(0, lastSlash);
|
||||
const parent = nodeMap[parentPath];
|
||||
if (parent?.children) {
|
||||
parent.children.push(node);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const item = getFile(name);
|
||||
if (item != null) {
|
||||
conflictingFiles.push({
|
||||
index: i,
|
||||
name: item.path,
|
||||
origin: {
|
||||
lastModified: file.modified || file.file?.lastModified,
|
||||
size: file.size,
|
||||
},
|
||||
dest: {
|
||||
lastModified: item.modified,
|
||||
size: item.size,
|
||||
},
|
||||
checked: ["origin"],
|
||||
});
|
||||
return roots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return conflict files from
|
||||
* @param files - flat upload list to check
|
||||
* @param basePath - server destination path (e.g. "/files/uploads/")
|
||||
*/
|
||||
export async function checkConflict(
|
||||
files: UploadList,
|
||||
basePath: string
|
||||
): Promise<ConflictingResource[]> {
|
||||
console.log(
|
||||
"Starting conflict check, " + files.length + " possible conflict found."
|
||||
);
|
||||
|
||||
const forest = flatToForest(files, basePath);
|
||||
if (forest.length === 0) return [];
|
||||
|
||||
// Single API call: fetch the entire server tree under basePath.
|
||||
let serverEntries: RecursiveEntry[] = [];
|
||||
try {
|
||||
serverEntries = await api.fetchAll(basePath);
|
||||
} catch {
|
||||
console.error(
|
||||
`Failed to fetch recursive server listing for ${basePath}. ` +
|
||||
`Assuming directory doesn't exist and skipping conflict check.`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Build a lookup map keyed by the normalised server path for O(1) access.
|
||||
// The server returns paths relative to the user's scope (e.g. "/uploads/foo.txt").
|
||||
// We strip the basePath prefix so the key matches the upload entry's fullPath.
|
||||
const normBase = removePrefix(basePath).replace(/\/+$/, "");
|
||||
const serverMap = new Map<string, RecursiveEntry>();
|
||||
for (const entry of serverEntries) {
|
||||
// entry.path is absolute from server root, e.g. "/uploads/sub/file.txt"
|
||||
// We need the relative part after normBase, e.g. "sub/file.txt"
|
||||
let rel = entry.path;
|
||||
if (rel.startsWith(normBase)) {
|
||||
rel = rel.slice(normBase.length);
|
||||
}
|
||||
// Strip leading slash so it matches fullPath format ("sub/file.txt")
|
||||
rel = rel.replace(/^\/+/, "");
|
||||
serverMap.set(rel, entry);
|
||||
}
|
||||
|
||||
const conflicts: ConflictingResource[] = [];
|
||||
|
||||
/**
|
||||
* Walk the upload tree and compare each file node against the
|
||||
* pre-fetched server map. Directories only need to be recursed
|
||||
* when they appear in the map (otherwise no child can conflict).
|
||||
*/
|
||||
function recursiveCheckConflict(nodes: UploadEntryWithChild[]): void {
|
||||
for (const node of nodes) {
|
||||
if (node.isDir && node.children) {
|
||||
// Only recurse if this directory exists on the server
|
||||
const dirKey = node.fullPath!.replace(/^\/+/, "");
|
||||
if (serverMap.has(dirKey)) {
|
||||
recursiveCheckConflict(node.children);
|
||||
}
|
||||
} else {
|
||||
// File – check for a conflict against the server map
|
||||
const fileKey = node.fullPath!.replace(/^\/+/, "");
|
||||
const serverEntry = serverMap.get(fileKey);
|
||||
|
||||
if (serverEntry) {
|
||||
conflicts.push({
|
||||
index: node.originalIndex,
|
||||
name: serverEntry.path,
|
||||
origin: {
|
||||
lastModified: node.file?.lastModified,
|
||||
size: node.size,
|
||||
},
|
||||
dest: {
|
||||
lastModified: serverEntry.modified,
|
||||
size: serverEntry.size,
|
||||
},
|
||||
checked: ["origin"],
|
||||
isSmallerOnServer: node.size > serverEntry.size,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return conflictingFiles;
|
||||
// Walk all root nodes synchronously against the pre-fetched data
|
||||
recursiveCheckConflict(forest);
|
||||
|
||||
console.log(conflicts.length + " conflicts found.");
|
||||
|
||||
conflicts.sort((a, b) => a.index - b.index);
|
||||
|
||||
return conflicts;
|
||||
}
|
||||
|
||||
export function scanFiles(dt: DataTransfer): Promise<UploadList | FileList> {
|
||||
|
|
|
|||
|
|
@ -647,7 +647,7 @@ const copyCut = (event: Event | KeyboardEvent): void => {
|
|||
});
|
||||
};
|
||||
|
||||
const paste = (event: Event) => {
|
||||
const paste = async (event: Event) => {
|
||||
if ((event.target as HTMLElement).tagName?.toLowerCase() === "input") return;
|
||||
|
||||
// TODO router location should it be
|
||||
|
|
@ -696,7 +696,8 @@ const paste = (event: Event) => {
|
|||
};
|
||||
}
|
||||
|
||||
const conflict = upload.checkConflict(items, fileStore.req!.items);
|
||||
const path = route.path.endsWith("/") ? route.path : route.path + "/";
|
||||
const conflict = await upload.checkConflict(items, path);
|
||||
|
||||
if (conflict.length > 0) {
|
||||
layoutStore.showHover({
|
||||
|
|
@ -799,7 +800,6 @@ const drop = async (event: DragEvent) => {
|
|||
}
|
||||
|
||||
const files: UploadList = (await upload.scanFiles(dt)) as UploadList;
|
||||
let items = fileStore.req.items;
|
||||
let path = route.path.endsWith("/") ? route.path : route.path + "/";
|
||||
|
||||
if (
|
||||
|
|
@ -812,13 +812,14 @@ const drop = async (event: DragEvent) => {
|
|||
path = el.__vue__.url;
|
||||
|
||||
try {
|
||||
items = (await api.fetch(path)).items;
|
||||
(await api.fetch(path)).items;
|
||||
} catch (error: any) {
|
||||
$showError(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const conflict = upload.checkConflict(files, items);
|
||||
const conflict = await upload.checkConflict(files, path);
|
||||
|
||||
const preselect = removePrefix(path) + (files[0].fullPath || files[0].name);
|
||||
|
||||
|
|
@ -856,7 +857,7 @@ const drop = async (event: DragEvent) => {
|
|||
fileStore.preselect = preselect;
|
||||
};
|
||||
|
||||
const uploadInput = (event: Event) => {
|
||||
const uploadInput = async (event: Event) => {
|
||||
const files = (event.currentTarget as HTMLInputElement)?.files;
|
||||
if (files === null) return;
|
||||
|
||||
|
|
@ -876,7 +877,7 @@ const uploadInput = (event: Event) => {
|
|||
}
|
||||
|
||||
const path = route.path.endsWith("/") ? route.path : route.path + "/";
|
||||
const conflict = upload.checkConflict(uploadFiles, fileStore.req!.items);
|
||||
const conflict = await upload.checkConflict(uploadFiles, path);
|
||||
|
||||
if (conflict.length > 0) {
|
||||
layoutStore.showHover({
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ func NewHandler(
|
|||
users.Handle("/{id:[0-9]+}", monkey(userGetHandler, "")).Methods("GET")
|
||||
users.Handle("/{id:[0-9]+}", monkey(userDeleteHandler, "")).Methods("DELETE")
|
||||
|
||||
api.PathPrefix("/resources/recursive").Handler(monkey(resourceGetRecursiveHandler, "/api/resources/recursive")).Methods("GET")
|
||||
api.PathPrefix("/resources").Handler(monkey(resourceGetHandler, "/api/resources")).Methods("GET")
|
||||
api.PathPrefix("/resources").Handler(monkey(resourceDeleteHandler(fileCache), "/api/resources")).Methods("DELETE")
|
||||
api.PathPrefix("/resources").Handler(monkey(resourcePostHandler(fileCache), "/api/resources")).Methods("POST")
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
"github.com/spf13/afero"
|
||||
|
|
@ -376,6 +377,69 @@ func patchAction(ctx context.Context, action, src, dst string, d *data, fileCach
|
|||
}
|
||||
}
|
||||
|
||||
// RecursiveEntry is a single file/directory entry returned by the recursive listing endpoint.
|
||||
type RecursiveEntry struct {
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
ModTime time.Time `json:"modified"`
|
||||
IsDir bool `json:"isDir"`
|
||||
}
|
||||
|
||||
// resourceGetRecursiveHandler returns a flat list of every file and directory
|
||||
// under the requested path, walking the tree recursively on the server side
|
||||
// so the client only needs a single HTTP call.
|
||||
var resourceGetRecursiveHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
rootPath := r.URL.Path
|
||||
if rootPath == "" {
|
||||
rootPath = "/"
|
||||
}
|
||||
|
||||
// Make sure the root itself exists and is a directory.
|
||||
info, err := d.user.Fs.Stat(rootPath)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return http.StatusBadRequest, fmt.Errorf("path is not a directory")
|
||||
}
|
||||
|
||||
entries := make([]RecursiveEntry, 0)
|
||||
|
||||
err = afero.Walk(d.user.Fs, rootPath, func(fPath string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil // skip entries we cannot read
|
||||
}
|
||||
|
||||
// Skip the root directory itself.
|
||||
if fPath == rootPath {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Respect user rules.
|
||||
if !d.Check(fPath) {
|
||||
if info.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
entries = append(entries, RecursiveEntry{
|
||||
Path: fPath,
|
||||
Name: info.Name(),
|
||||
Size: info.Size(),
|
||||
ModTime: info.ModTime(),
|
||||
IsDir: info.IsDir(),
|
||||
})
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
return renderJSON(w, r, entries)
|
||||
})
|
||||
|
||||
type DiskUsageResponse struct {
|
||||
Total uint64 `json:"total"`
|
||||
Used uint64 `json:"used"`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue