mirror of
https://github.com/phantomptr/ps5upload.git
synced 2026-10-19 02:20:02 -05:00
console files: paste decides "the item itself" by dev+ino, Stop/close skip a waiting conflict
/api/fs/stat returns dev and ino (lstat of the accepted path) so a case-insensitive exFAT spelling of the same folder or item is never offered Replace (which deleted the item first) and a cut there is a no-op. Jobs Stop left running stay in the cut clipboard; Stop during a Replace delete says so; Stop and closing Files answer a pending conflict with Skip; one combined replace-all body. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS
This commit is contained in:
1 parent
910321cb4a
commit
dccc77f38b
49 files changed
+320
-13
No files matched your search
@@ -20,7 +20,13 @@ export function ConflictDialog({ conflict, onAnswer }: { conflict: Conflict; onA
|
||||
return (
|
||||
<ConfirmDialog
|
||||
title={t(all ? "cw_files_conflict_replace_all_title" : "cw_files_conflict_replace_title", { name })}
|
||||
body={t(conflict.destDir ? "cw_files_conflict_replace_dir_body" : "cw_files_conflict_replace_file_body")}
|
||||
body={t(
|
||||
all
|
||||
? "cw_files_conflict_replace_all_body"
|
||||
: conflict.destDir
|
||||
? "cw_files_conflict_replace_dir_body"
|
||||
: "cw_files_conflict_replace_file_body",
|
||||
)}
|
||||
confirmLabel={t("cw_files_conflict_replace")}
|
||||
danger
|
||||
onConfirm={() => onAnswer({ choice: "replace", all })}
|
||||
|
||||
@@ -381,7 +381,7 @@ describe("Files listing controls", () => {
|
||||
});
|
||||
|
||||
it("paste messages: skipped, already here, failures", () => {
|
||||
const base = { started: [], moved: [], failed: [], jobFailed: [], skipped: [], done: [], declined: [], here: [] };
|
||||
const base = { started: [], moved: [], failed: [], jobFailed: [], skipped: [], done: [], left: [], halted: [], declined: [], here: [] };
|
||||
expect(pasteMessage({ ...base, done: ["/a"] })?.text).toBe(t("cw_files_done"));
|
||||
expect(pasteMessage({ ...base, done: ["/a"], declined: ["/b", "/c"] })?.text).toBe(t("cw_files_paste_done_skipped", { n: 2 }));
|
||||
expect(pasteMessage({ ...base, here: ["/a"] })?.text).toBe(t("cw_files_paste_here_already"));
|
||||
@@ -390,5 +390,12 @@ describe("Files listing controls", () => {
|
||||
t("cw_files_err_into_itself"),
|
||||
);
|
||||
expect(filesErrorText(new ApiError(500, "replace_delete_failed"))).toBe(t("cw_files_replace_failed"));
|
||||
// Stop during a Replace's delete: that item is named, the rest counted.
|
||||
expect(pasteMessage({ ...base, halted: ["/data/a/G"], skipped: ["/data/a/G"] })?.text).toBe(
|
||||
t("cw_files_paste_halted", { name: "G" }),
|
||||
);
|
||||
expect(pasteMessage({ ...base, halted: ["/data/a/G"], skipped: ["/data/a/G", "/data/a/H"] })?.text).toBe(
|
||||
`${t("cw_files_paste_halted", { name: "G" })} ${t("cw_files_batch_stopped", { n: 1 })}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -55,6 +55,7 @@ import {
|
||||
dropFromClipboard,
|
||||
getClipboard,
|
||||
setClipboard,
|
||||
skipPendingConflict,
|
||||
useClipboard,
|
||||
useConflict,
|
||||
type Clip,
|
||||
@@ -114,6 +115,11 @@ export function batchMessage(r: BatchResult, op?: Busy["op"]): Message | null {
|
||||
|
||||
/** The line a finished paste leaves: batchMessage's, or what was skipped / already there. */
|
||||
export function pasteMessage(r: PasteResult): Message | null {
|
||||
if (r.halted.length && !r.failed.length && !r.jobFailed.length) {
|
||||
const rest = r.skipped.length - r.halted.length;
|
||||
const text = t("cw_files_paste_halted", { name: baseName(r.halted[0]) });
|
||||
return { tone: "info", text: rest > 0 ? `${text} ${t("cw_files_batch_stopped", { n: rest })}` : text };
|
||||
}
|
||||
if (r.failed.length || r.jobFailed.length || r.skipped.length) return batchMessage(r);
|
||||
if (!r.done.length && !r.declined.length && r.here.length) return { tone: "info", text: t("cw_files_paste_here_already") };
|
||||
if (r.declined.length) return { tone: "info", text: t("cw_files_paste_done_skipped", { n: r.declined.length }) };
|
||||
@@ -783,6 +789,12 @@ export function FilesView(p: FilesViewProps) {
|
||||
);
|
||||
}
|
||||
|
||||
/** Stop: the batch ends before its next item, and a conflict it waits on is answered Skip. */
|
||||
export function stopBatchAndConflict(): void {
|
||||
stopBatch();
|
||||
skipPendingConflict();
|
||||
}
|
||||
|
||||
/* ---- the section ---- */
|
||||
|
||||
function useVolumes(path: string | null, tick: number): { volumes: Volume[] | null; error: boolean } {
|
||||
@@ -863,6 +875,8 @@ export function Files({ ctx }: { ctx: SectionContext }) {
|
||||
mounted.current = true;
|
||||
return () => {
|
||||
mounted.current = false;
|
||||
// A paste conflict nobody can answer now: skip it (and the rest), so the batch ends.
|
||||
skipPendingConflict();
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -1355,7 +1369,7 @@ export function Files({ ctx }: { ctx: SectionContext }) {
|
||||
onUnmount={onUnmount}
|
||||
onGoTo={onGoTo}
|
||||
gameRunning={gameRunning}
|
||||
onStopBatch={stopBatch}
|
||||
onStopBatch={stopBatchAndConflict}
|
||||
clip={clip}
|
||||
conflict={conflict}
|
||||
onClip={onClip}
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
* folders change and while another section is open, for the whole page
|
||||
* session; a paste that meets a conflict while Files is closed waits until
|
||||
* Files is open again to ask. Nothing here is saved: a reload empties it.
|
||||
*
|
||||
* Ruling R22: ○ (browser Back) never clears the clipboard, at any level; only
|
||||
* the bar's Clear does, or a cut whose items all moved.
|
||||
*/
|
||||
import { useEffect, useState } from "react";
|
||||
import type { Conflict, Decision, PasteMode } from "./paste";
|
||||
@@ -94,6 +97,14 @@ export function askConflict(conflict: Conflict): Promise<Decision> {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Answer a waiting conflict with Skip for it and every later one (Stop, or
|
||||
* Files closing): the batch never sits busy behind a dialog nobody can see.
|
||||
*/
|
||||
export function skipPendingConflict(): void {
|
||||
pending.get()?.answer({ choice: "skip", all: true });
|
||||
}
|
||||
|
||||
export function currentConflict(): PendingConflict | null {
|
||||
return pending.get();
|
||||
}
|
||||
|
||||
@@ -342,3 +342,115 @@ describe("paste as the page's batch", () => {
|
||||
expect(currentBatch()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("the item itself, by identity (exFAT is case-insensitive)", () => {
|
||||
/** /mnt/usb0 answers any letter case with the same entry (dev + ino), like exFAT. */
|
||||
function exfat(fs: Record<string, "file" | "dir">, ids = true) {
|
||||
const key = (p: string) => (p.startsWith("/mnt/usb0") ? p.toLowerCase() : p);
|
||||
const norm: Record<string, "file" | "dir"> = {};
|
||||
for (const [p, k] of Object.entries(fs)) norm[key(p)] = k;
|
||||
let n = 0;
|
||||
mockCall.mockImplementation(async (route: string, body?: unknown) => {
|
||||
const b = body as Record<string, string>;
|
||||
if (route === "/api/fs/stat") {
|
||||
const k = norm[key(b.path)];
|
||||
if (!k) throw new ApiError(404, "not_found");
|
||||
return (ids ? { kind: k, dev: "7", ino: key(b.path) } : { kind: k }) as never;
|
||||
}
|
||||
if (route === "/api/fs/delete") return { job: `del${++n}` } as never;
|
||||
if (route === "/api/fs/copy") return { job: `job${++n}` } as never;
|
||||
return {} as never;
|
||||
});
|
||||
}
|
||||
|
||||
it("a cut into the same folder spelled differently is 'already here', nothing is sent", async () => {
|
||||
exfat({ "/mnt/usb0/Games": "dir", "/mnt/usb0/Games/x": "file" });
|
||||
const a = answers();
|
||||
const r = await pasteItems([{ path: "/mnt/usb0/Games/x", dir: false }], "/mnt/usb0/games", "cut", {
|
||||
decide: a.decide,
|
||||
volumes: VOLS,
|
||||
});
|
||||
expect(a.asked).toEqual([]);
|
||||
expect(routes()).toEqual([]);
|
||||
expect(r.here).toEqual(["/mnt/usb0/Games/x"]);
|
||||
});
|
||||
|
||||
it("the destination found is the item itself: Replace is never offered, Apply-to-all Replace skips it", async () => {
|
||||
exfat({ "/mnt/usb0/Games": "dir", "/mnt/usb0/Games/x": "file", "/mnt/usb0/Games/y": "file" });
|
||||
const a = answers({ choice: "replace", all: true });
|
||||
const r = await pasteItems(
|
||||
[
|
||||
{ path: "/mnt/usb0/Games/x", dir: false },
|
||||
{ path: "/mnt/usb0/Games/y", dir: false },
|
||||
],
|
||||
"/mnt/usb0/games",
|
||||
"copy",
|
||||
{ decide: a.decide, volumes: VOLS },
|
||||
);
|
||||
expect(a.asked[0].canReplace).toBe(false);
|
||||
expect(routes()).toEqual([]); // no delete, no copy
|
||||
expect(r.declined).toEqual(["/mnt/usb0/Games/x", "/mnt/usb0/Games/y"]);
|
||||
});
|
||||
|
||||
it("a cut whose folder ids differ but the destination is the item itself: no-op, never deleted", async () => {
|
||||
exfat({ "/mnt/usb0/Games/x": "file" });
|
||||
// The folders' stats fail (no ids to compare): the item's own stat still catches it.
|
||||
const base = mockCall.getMockImplementation()!;
|
||||
mockCall.mockImplementation(async (route: string, body?: unknown) => {
|
||||
const p = (body as { path?: string })?.path ?? "";
|
||||
if (route === "/api/fs/stat" && !p.endsWith("/x")) throw new ApiError(500, "io");
|
||||
return base(route, body as never);
|
||||
});
|
||||
const r = await pasteItems([{ path: "/mnt/usb0/Games/x", dir: false }], "/mnt/usb0/GAMES", "cut", {
|
||||
decide: answers({ choice: "replace", all: false }).decide,
|
||||
volumes: VOLS,
|
||||
});
|
||||
expect(routes()).toEqual([]);
|
||||
expect(r.here).toEqual(["/mnt/usb0/Games/x"]);
|
||||
});
|
||||
|
||||
it("an older helper without ids falls back to the path compare", async () => {
|
||||
exfat({ "/data/a/x": "file", "/data/b/x": "file" }, false);
|
||||
const a = answers({ choice: "replace", all: false });
|
||||
await pasteItems([{ path: "/data/a/x", dir: false }], "/data/b", "copy", { decide: a.decide });
|
||||
expect(a.asked[0].canReplace).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Stop leaves a job running", () => {
|
||||
it("a cut whose job Stop left running is not counted done (it stays in the clipboard)", async () => {
|
||||
fakeConsole({ "/mnt/usb0/G": "dir" });
|
||||
const r = await pasteItems([{ path: "/mnt/usb0/G", dir: true }], "/data/b", "cut", { decide: answers().decide, volumes: VOLS }, {
|
||||
waitJob: async () => "left",
|
||||
});
|
||||
expect(r.left).toEqual(["/mnt/usb0/G"]);
|
||||
expect(r.done).toEqual([]);
|
||||
});
|
||||
|
||||
it("Stop during a replace delete is reported as halted", async () => {
|
||||
fakeConsole({ "/data/a/G": "dir", "/data/b/G": "dir" });
|
||||
const r = await pasteItems([{ path: "/data/a/G", dir: true }], "/data/b", "copy", {
|
||||
decide: answers({ choice: "replace", all: false }).decide,
|
||||
}, { waitJob: async () => "left" });
|
||||
expect(r.halted).toEqual(["/data/a/G"]);
|
||||
expect(r.left).toEqual([]);
|
||||
});
|
||||
|
||||
it("Stop / Files closing answers a waiting conflict with Skip for the rest", async () => {
|
||||
const { skipPendingConflict } = await import("./clipboard");
|
||||
fakeConsole({ "/data/a/1": "file", "/data/a/2": "file", "/data/b/1": "file", "/data/b/2": "file" });
|
||||
const run = pasteItems(
|
||||
["1", "2"].map((n) => ({ path: `/data/a/${n}`, dir: false })),
|
||||
"/data/b",
|
||||
"copy",
|
||||
{ decide: askConflict },
|
||||
);
|
||||
for (let i = 0; i < 20 && !currentConflict(); i++) await Promise.resolve();
|
||||
expect(currentConflict()).not.toBeNull();
|
||||
skipPendingConflict();
|
||||
const r = await run;
|
||||
expect(r.declined).toEqual(["/data/a/1", "/data/a/2"]);
|
||||
expect(currentConflict()).toBeNull();
|
||||
expect(routes()).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -18,6 +18,14 @@
|
||||
* refuses the rename (cross_mount) and a copy job with the move flag does it.
|
||||
* A folder never goes into itself (IntoItselfError); a cut item pasted into
|
||||
* its own folder stays as it is.
|
||||
*
|
||||
* "The item itself" is decided by identity (/api/fs/stat's dev + ino), not by
|
||||
* the path's spelling: exFAT USB drives are case-insensitive, so a cut of
|
||||
* /mnt/usb0/Games/x pasted into a typed /mnt/usb0/games finds x itself at the
|
||||
* destination. Offering Replace there would delete the item (delete-first), so
|
||||
* an identical entry is never replaceable, and for a cut it is "already here".
|
||||
* The destination folder is compared with each item's folder the same way. A
|
||||
* helper without ids falls back to the path compare.
|
||||
*/
|
||||
import { ApiError, call } from "../api";
|
||||
import {
|
||||
@@ -63,6 +71,10 @@ export interface PasteItem {
|
||||
export interface PasteResult extends BatchResult {
|
||||
/** Items now in the folder (moved, or their job ended done). */
|
||||
done: string[];
|
||||
/** Items whose job Stop left running (not known to be done: a cut keeps them in the clipboard). */
|
||||
left: string[];
|
||||
/** Items not pasted because Stop came while the delete for their Replace ran (that delete runs on). */
|
||||
halted: string[];
|
||||
/** Conflicts the user skipped. */
|
||||
declined: string[];
|
||||
/** Cut items already in this folder (nothing to do). */
|
||||
@@ -79,8 +91,16 @@ export interface PasteDeps {
|
||||
/** Keep both's tries before it gives up. */
|
||||
const KEEP_BOTH_TRIES = 999;
|
||||
|
||||
interface Stat {
|
||||
export interface Stat {
|
||||
kind: string;
|
||||
/** Device and inode (decimal strings), from a helper that sends them. */
|
||||
dev?: string;
|
||||
ino?: string;
|
||||
}
|
||||
|
||||
/** The same entry on disk (both stats carry ids and they match). */
|
||||
export function sameEntry(a: Stat | null | undefined, b: Stat | null | undefined): boolean {
|
||||
return !!a && !!b && a.dev != null && a.ino != null && a.dev === b.dev && a.ino === b.ino;
|
||||
}
|
||||
|
||||
/** The entry at `path`, or null when nothing is there (404). */
|
||||
@@ -138,22 +158,55 @@ export async function pasteItems(
|
||||
const here: string[] = [];
|
||||
/** The item whose Replace delete was left running by Stop: it was not pasted. */
|
||||
const halted: string[] = [];
|
||||
/** Items whose job was still running when Stop let go of it. */
|
||||
const leftRunning: string[] = [];
|
||||
const kinds = new Map(items.map((i) => [i.path, i.dir]));
|
||||
const paths = items.map((i) => i.path);
|
||||
let always: ConflictChoice | null = null;
|
||||
let current = "";
|
||||
/** Stats of folders (the destination, the items' folders), read once each. */
|
||||
const folderStats = new Map<string, Promise<Stat | null>>();
|
||||
const folderStat = (p: string) => {
|
||||
let s = folderStats.get(p);
|
||||
if (!s) {
|
||||
s = statOrNull(p).catch(() => null);
|
||||
folderStats.set(p, s);
|
||||
}
|
||||
return s;
|
||||
};
|
||||
const wait = opts?.waitJob;
|
||||
const eachOpts: BatchOptions | undefined = opts && {
|
||||
...opts,
|
||||
waitJob: wait
|
||||
? async (id) => {
|
||||
const end = await wait(id);
|
||||
if (end === "left") leftRunning.push(current);
|
||||
return end;
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const r = await each(paths, opts, async (item, res) => {
|
||||
const r = await each(paths, eachOpts, async (item, res) => {
|
||||
current = item;
|
||||
if (isInside(dir, item)) throw new IntoItselfError(item);
|
||||
if (mode === "cut" && parentPath(item) === dir) {
|
||||
here.push(item);
|
||||
return;
|
||||
if (mode === "cut") {
|
||||
const from = parentPath(item);
|
||||
if (from === dir || sameEntry(await folderStat(from), await folderStat(dir))) {
|
||||
here.push(item);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const isDir = kinds.get(item) ?? false;
|
||||
let dest = joinPath(dir, baseName(item));
|
||||
let overwrite = false;
|
||||
const existing = await statOrNull(dest);
|
||||
if (existing) {
|
||||
const canReplace = !isInside(item, dest);
|
||||
const self = sameEntry(existing, await statOrNull(item));
|
||||
if (self && mode === "cut") {
|
||||
here.push(item);
|
||||
return;
|
||||
}
|
||||
const canReplace = !self && !isInside(item, dest);
|
||||
let choice = always;
|
||||
if (!choice) {
|
||||
const d = await deps.decide({
|
||||
@@ -200,11 +253,15 @@ export async function pasteItems(
|
||||
return startCopy(item, dest, true, res, overwrite);
|
||||
});
|
||||
|
||||
const notDone = new Set<string>(declined.concat(here, halted, r.skipped, r.jobFailed, r.failed.map((f) => f.item)));
|
||||
const notDone = new Set<string>(
|
||||
declined.concat(here, halted, leftRunning, r.skipped, r.jobFailed, r.failed.map((f) => f.item)),
|
||||
);
|
||||
return {
|
||||
...r,
|
||||
skipped: halted.concat(r.skipped),
|
||||
done: paths.filter((p) => !notDone.has(p)),
|
||||
left: leftRunning,
|
||||
halted,
|
||||
declined,
|
||||
here,
|
||||
};
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"cw_files_conflict_body_self": "هذا هو العنصر نفسه. تخطَّه أو احتفظ بالاثنين (تحصل النسخة على اسم جديد).",
|
||||
"cw_files_conflict_keep": "الاحتفاظ بالاثنين",
|
||||
"cw_files_conflict_replace": "استبدال",
|
||||
"cw_files_conflict_replace_all_body": "يُكتب فوق الملفات الموجودة هنا؛ وتُحذف المجلدات بكل محتواها أولًا. لا يمكن التراجع عن ذلك.",
|
||||
"cw_files_conflict_replace_all_title": "استبدال «{name}» وكل التعارضات التالية؟",
|
||||
"cw_files_conflict_replace_dir_body": "يُحذف الموجود هنا بكل محتواه أولًا. لا يمكن التراجع عن ذلك.",
|
||||
"cw_files_conflict_replace_file_body": "سيُكتب فوق الملف الموجود. لا يمكن التراجع عن ذلك.",
|
||||
@@ -135,6 +136,7 @@
|
||||
"cw_files_order_asc": "تصاعدي",
|
||||
"cw_files_order_desc": "تنازلي",
|
||||
"cw_files_paste_done_skipped": "تم. تخطي {n}.",
|
||||
"cw_files_paste_halted": "توقف. لم يُلصق «{name}»؛ ولا يزال العنصر الذي كان سيستبدله قيد الحذف.",
|
||||
"cw_files_paste_here_already": "موجود في هذا المجلد بالفعل.",
|
||||
"cw_files_path": "مسار المجلد",
|
||||
"cw_files_path_more": "إظهار المجلدات المخفية ({n})",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"cw_files_conflict_body_self": "এটি আইটেমটি নিজেই। বাদ দিন, অথবা দুটোই রাখুন (কপিটি নতুন নাম পাবে)।",
|
||||
"cw_files_conflict_keep": "দুটোই রাখুন",
|
||||
"cw_files_conflict_replace": "প্রতিস্থাপন",
|
||||
"cw_files_conflict_replace_all_body": "এখানে থাকা ফাইলগুলো ওভাররাইট হবে; ফোল্ডারগুলো ভেতরের সবকিছুসহ আগে মুছে ফেলা হবে। এটি ফেরানো যাবে না।",
|
||||
"cw_files_conflict_replace_all_title": "“{name}” এবং পরের সব দ্বন্দ্ব প্রতিস্থাপন করবেন?",
|
||||
"cw_files_conflict_replace_dir_body": "এখানে যা আছে, ভেতরের সবকিছুসহ, আগে মুছে ফেলা হবে। এটি ফেরানো যাবে না।",
|
||||
"cw_files_conflict_replace_file_body": "এখানে থাকা ফাইলটি ওভাররাইট হবে। এটি ফেরানো যাবে না।",
|
||||
@@ -135,6 +136,7 @@
|
||||
"cw_files_order_asc": "ঊর্ধ্বক্রম",
|
||||
"cw_files_order_desc": "নিম্নক্রম",
|
||||
"cw_files_paste_done_skipped": "হয়ে গেছে। {n}টি বাদ দেওয়া হয়েছে।",
|
||||
"cw_files_paste_halted": "থামানো হয়েছে। “{name}” পেস্ট হয়নি; যে আইটেমটি এটি প্রতিস্থাপন করত সেটি এখনও মোছা হচ্ছে।",
|
||||
"cw_files_paste_here_already": "আগে থেকেই এই ফোল্ডারে আছে।",
|
||||
"cw_files_path": "ফোল্ডারের পাথ",
|
||||
"cw_files_path_more": "লুকানো {n}টি ফোল্ডার দেখান",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"cw_files_conflict_body_self": "Das ist das Element selbst. Überspringen oder beide behalten (die Kopie bekommt einen neuen Namen).",
|
||||
"cw_files_conflict_keep": "Beide behalten",
|
||||
"cw_files_conflict_replace": "Ersetzen",
|
||||
"cw_files_conflict_replace_all_body": "Vorhandene Dateien werden überschrieben; Ordner werden samt Inhalt zuerst gelöscht. Das lässt sich nicht rückgängig machen.",
|
||||
"cw_files_conflict_replace_all_title": "„{name}“ und alle weiteren Konflikte ersetzen?",
|
||||
"cw_files_conflict_replace_dir_body": "Was schon hier ist, wird samt Inhalt zuerst gelöscht. Das lässt sich nicht rückgängig machen.",
|
||||
"cw_files_conflict_replace_file_body": "Die vorhandene Datei wird überschrieben. Das lässt sich nicht rückgängig machen.",
|
||||
@@ -135,6 +136,7 @@
|
||||
"cw_files_order_asc": "aufsteigend",
|
||||
"cw_files_order_desc": "absteigend",
|
||||
"cw_files_paste_done_skipped": "Fertig. {n} übersprungen.",
|
||||
"cw_files_paste_halted": "Gestoppt. „{name}“ wurde nicht eingefügt; das Element, das es ersetzen sollte, wird noch gelöscht.",
|
||||
"cw_files_paste_here_already": "Schon in diesem Ordner.",
|
||||
"cw_files_path": "Ordnerpfad",
|
||||
"cw_files_path_more": "{n} ausgeblendete Ordner anzeigen",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"cw_files_conflict_body_self": "This is the item itself. Skip it, or keep both (the copy gets a new name).",
|
||||
"cw_files_conflict_keep": "Keep both",
|
||||
"cw_files_conflict_replace": "Replace",
|
||||
"cw_files_conflict_replace_all_body": "Files already here are overwritten; folders, and everything in them, are deleted first. This can't be undone.",
|
||||
"cw_files_conflict_replace_all_title": "Replace “{name}” and every later conflict?",
|
||||
"cw_files_conflict_replace_dir_body": "What is already here, and everything in it, is deleted first. This can't be undone.",
|
||||
"cw_files_conflict_replace_file_body": "The file already here is overwritten. This can't be undone.",
|
||||
@@ -135,6 +136,7 @@
|
||||
"cw_files_order_asc": "ascending",
|
||||
"cw_files_order_desc": "descending",
|
||||
"cw_files_paste_done_skipped": "Done. {n} skipped.",
|
||||
"cw_files_paste_halted": "Stopped. “{name}” was not pasted; the item it was replacing is still being deleted.",
|
||||
"cw_files_paste_here_already": "Already in this folder.",
|
||||
"cw_files_path": "Folder path",
|
||||
"cw_files_path_more": "Show {n} hidden folders",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"cw_files_conflict_body_self": "Es el propio elemento. Omítelo o conserva ambos (la copia recibe un nombre nuevo).",
|
||||
"cw_files_conflict_keep": "Conservar ambos",
|
||||
"cw_files_conflict_replace": "Reemplazar",
|
||||
"cw_files_conflict_replace_all_body": "Los archivos que ya están aquí se sobrescriben; las carpetas, con todo su contenido, se eliminan primero. No se puede deshacer.",
|
||||
"cw_files_conflict_replace_all_title": "¿Reemplazar «{name}» y todos los conflictos siguientes?",
|
||||
"cw_files_conflict_replace_dir_body": "Lo que ya está aquí, con todo su contenido, se elimina primero. No se puede deshacer.",
|
||||
"cw_files_conflict_replace_file_body": "El archivo que ya está aquí se sobrescribe. No se puede deshacer.",
|
||||
@@ -135,6 +136,7 @@
|
||||
"cw_files_order_asc": "ascendente",
|
||||
"cw_files_order_desc": "descendente",
|
||||
"cw_files_paste_done_skipped": "Listo. {n} omitidos.",
|
||||
"cw_files_paste_halted": "Detenido. «{name}» no se pegó; el elemento que iba a reemplazar todavía se está eliminando.",
|
||||
"cw_files_paste_here_already": "Ya está en esta carpeta.",
|
||||
"cw_files_path": "Ruta de la carpeta",
|
||||
"cw_files_path_more": "Mostrar {n} carpetas ocultas",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"cw_files_conflict_body_self": "این خود همان مورد است. رد شوید یا هر دو را نگه دارید (کپی نام تازه میگیرد).",
|
||||
"cw_files_conflict_keep": "نگه داشتن هر دو",
|
||||
"cw_files_conflict_replace": "جایگزینی",
|
||||
"cw_files_conflict_replace_all_body": "فایلهای موجود بازنویسی میشوند؛ پوشهها با همه محتوایشان اول حذف میشوند. این کار برگشتپذیر نیست.",
|
||||
"cw_files_conflict_replace_all_title": "«{name}» و همه تداخلهای بعدی جایگزین شوند؟",
|
||||
"cw_files_conflict_replace_dir_body": "آنچه اینجا هست، با همه محتوایش، اول حذف میشود. این کار برگشتپذیر نیست.",
|
||||
"cw_files_conflict_replace_file_body": "فایل موجود بازنویسی میشود. این کار برگشتپذیر نیست.",
|
||||
@@ -135,6 +136,7 @@
|
||||
"cw_files_order_asc": "صعودی",
|
||||
"cw_files_order_desc": "نزولی",
|
||||
"cw_files_paste_done_skipped": "انجام شد. {n} مورد رد شد.",
|
||||
"cw_files_paste_halted": "متوقف شد. «{name}» چسبانده نشد؛ موردی که قرار بود جایگزینش شود هنوز در حال حذف است.",
|
||||
"cw_files_paste_here_already": "از قبل در همین پوشه است.",
|
||||
"cw_files_path": "مسیر پوشه",
|
||||
"cw_files_path_more": "نمایش {n} پوشهٔ پنهان",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"cw_files_conflict_body_self": "C’est l’élément lui-même. Ignorez-le ou gardez les deux (la copie reçoit un nouveau nom).",
|
||||
"cw_files_conflict_keep": "Garder les deux",
|
||||
"cw_files_conflict_replace": "Remplacer",
|
||||
"cw_files_conflict_replace_all_body": "Les fichiers déjà présents sont écrasés ; les dossiers, avec tout leur contenu, sont d’abord supprimés. Action irréversible.",
|
||||
"cw_files_conflict_replace_all_title": "Remplacer « {name} » et tous les conflits suivants ?",
|
||||
"cw_files_conflict_replace_dir_body": "Ce qui est déjà ici, avec tout son contenu, est d’abord supprimé. Action irréversible.",
|
||||
"cw_files_conflict_replace_file_body": "Le fichier déjà présent est écrasé. Action irréversible.",
|
||||
@@ -135,6 +136,7 @@
|
||||
"cw_files_order_asc": "croissant",
|
||||
"cw_files_order_desc": "décroissant",
|
||||
"cw_files_paste_done_skipped": "Terminé. {n} ignorés.",
|
||||
"cw_files_paste_halted": "Arrêté. « {name} » n’a pas été collé ; l’élément qu’il devait remplacer est encore en cours de suppression.",
|
||||
"cw_files_paste_here_already": "Déjà dans ce dossier.",
|
||||
"cw_files_path": "Chemin du dossier",
|
||||
"cw_files_path_more": "Afficher {n} dossiers masqués",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"cw_files_conflict_body_self": "यह वही आइटम है। छोड़ें, या दोनों रखें (कॉपी को नया नाम मिलेगा)।",
|
||||
"cw_files_conflict_keep": "दोनों रखें",
|
||||
"cw_files_conflict_replace": "बदलें",
|
||||
"cw_files_conflict_replace_all_body": "यहाँ मौजूद फ़ाइलें ओवरराइट होंगी; फ़ोल्डर, उनके अंदर की हर चीज़ समेत, पहले हटा दिए जाएँगे। इसे पूर्ववत नहीं किया जा सकता।",
|
||||
"cw_files_conflict_replace_all_title": "“{name}” और आगे के सभी टकराव बदलें?",
|
||||
"cw_files_conflict_replace_dir_body": "यहाँ जो है, उसके अंदर की हर चीज़ समेत, पहले हटा दिया जाएगा। इसे पूर्ववत नहीं किया जा सकता।",
|
||||
"cw_files_conflict_replace_file_body": "यहाँ मौजूद फ़ाइल ओवरराइट हो जाएगी। इसे पूर्ववत नहीं किया जा सकता।",
|
||||
@@ -135,6 +136,7 @@
|
||||
"cw_files_order_asc": "बढ़ता",
|
||||
"cw_files_order_desc": "घटता",
|
||||
"cw_files_paste_done_skipped": "हो गया। {n} छोड़े गए।",
|
||||
"cw_files_paste_halted": "रोका गया। “{name}” पेस्ट नहीं हुआ; जिस आइटम को यह बदलने वाला था, वह अभी हटाया जा रहा है।",
|
||||
"cw_files_paste_here_already": "पहले से इसी फ़ोल्डर में है।",
|
||||
"cw_files_path": "फ़ोल्डर पथ",
|
||||
"cw_files_path_more": "{n} छिपे फ़ोल्डर दिखाएँ",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"cw_files_conflict_body_self": "Ez maga az elem. Hagyd ki, vagy tartsd meg mindkettőt (a másolat új nevet kap).",
|
||||
"cw_files_conflict_keep": "Mindkettő megtartása",
|
||||
"cw_files_conflict_replace": "Csere",
|
||||
"cw_files_conflict_replace_all_body": "Az itt lévő fájlok felülíródnak; a mappák a teljes tartalmukkal együtt előbb törlődnek. Ez nem vonható vissza.",
|
||||
"cw_files_conflict_replace_all_title": "Lecseréled: „{name}”, és minden további ütközést?",
|
||||
"cw_files_conflict_replace_dir_body": "Ami már itt van, a teljes tartalmával együtt előbb törlődik. Ez nem vonható vissza.",
|
||||
"cw_files_conflict_replace_file_body": "Az itt lévő fájl felülíródik. Ez nem vonható vissza.",
|
||||
@@ -135,6 +136,7 @@
|
||||
"cw_files_order_asc": "növekvő",
|
||||
"cw_files_order_desc": "csökkenő",
|
||||
"cw_files_paste_done_skipped": "Kész. {n} kihagyva.",
|
||||
"cw_files_paste_halted": "Leállítva. „{name}” nem lett beillesztve; a lecserélendő elem törlése még folyamatban van.",
|
||||
"cw_files_paste_here_already": "Már ebben a mappában van.",
|
||||
"cw_files_path": "Mappa elérési útja",
|
||||
"cw_files_path_more": "{n} rejtett mappa megjelenítése",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"cw_files_conflict_body_self": "Ini item itu sendiri. Lewati, atau simpan keduanya (salinannya diberi nama baru).",
|
||||
"cw_files_conflict_keep": "Simpan keduanya",
|
||||
"cw_files_conflict_replace": "Ganti",
|
||||
"cw_files_conflict_replace_all_body": "File yang sudah ada akan ditimpa; folder beserta seluruh isinya dihapus terlebih dahulu. Ini tidak bisa dibatalkan.",
|
||||
"cw_files_conflict_replace_all_title": "Ganti “{name}” dan semua konflik berikutnya?",
|
||||
"cw_files_conflict_replace_dir_body": "Yang sudah ada di sini, beserta seluruh isinya, dihapus terlebih dahulu. Ini tidak bisa dibatalkan.",
|
||||
"cw_files_conflict_replace_file_body": "File yang sudah ada akan ditimpa. Ini tidak bisa dibatalkan.",
|
||||
@@ -135,6 +136,7 @@
|
||||
"cw_files_order_asc": "naik",
|
||||
"cw_files_order_desc": "turun",
|
||||
"cw_files_paste_done_skipped": "Selesai. {n} dilewati.",
|
||||
"cw_files_paste_halted": "Dihentikan. “{name}” tidak ditempel; item yang akan diganti masih sedang dihapus.",
|
||||
"cw_files_paste_here_already": "Sudah ada di folder ini.",
|
||||
"cw_files_path": "Jalur folder",
|
||||
"cw_files_path_more": "Tampilkan {n} folder tersembunyi",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"cw_files_conflict_body_self": "È l’elemento stesso. Saltalo o mantieni entrambi (la copia riceve un nuovo nome).",
|
||||
"cw_files_conflict_keep": "Mantieni entrambi",
|
||||
"cw_files_conflict_replace": "Sostituisci",
|
||||
"cw_files_conflict_replace_all_body": "I file già presenti vengono sovrascritti; le cartelle, con tutto il contenuto, vengono prima eliminate. Non si può annullare.",
|
||||
"cw_files_conflict_replace_all_title": "Sostituire «{name}» e tutti i conflitti successivi?",
|
||||
"cw_files_conflict_replace_dir_body": "Ciò che è già qui, con tutto il contenuto, viene prima eliminato. Non si può annullare.",
|
||||
"cw_files_conflict_replace_file_body": "Il file già presente viene sovrascritto. Non si può annullare.",
|
||||
@@ -135,6 +136,7 @@
|
||||
"cw_files_order_asc": "crescente",
|
||||
"cw_files_order_desc": "decrescente",
|
||||
"cw_files_paste_done_skipped": "Fatto. {n} saltati.",
|
||||
"cw_files_paste_halted": "Interrotto. «{name}» non è stato incollato; l’elemento da sostituire è ancora in fase di eliminazione.",
|
||||
"cw_files_paste_here_already": "Già in questa cartella.",
|
||||
"cw_files_path": "Percorso della cartella",
|
||||
"cw_files_path_more": "Mostra {n} cartelle nascoste",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"cw_files_conflict_body_self": "これは項目そのものです。スキップするか、両方を残してください(コピーに新しい名前を付けます)。",
|
||||
"cw_files_conflict_keep": "両方を残す",
|
||||
"cw_files_conflict_replace": "置き換える",
|
||||
"cw_files_conflict_replace_all_body": "既存のファイルは上書きされ、フォルダーは中身ごと先に削除されます。元に戻せません。",
|
||||
"cw_files_conflict_replace_all_title": "「{name}」と以降のすべての競合を置き換えますか?",
|
||||
"cw_files_conflict_replace_dir_body": "既存のものは中身ごと先に削除されます。元に戻せません。",
|
||||
"cw_files_conflict_replace_file_body": "既存のファイルは上書きされます。元に戻せません。",
|
||||
@@ -135,6 +136,7 @@
|
||||
"cw_files_order_asc": "昇順",
|
||||
"cw_files_order_desc": "降順",
|
||||
"cw_files_paste_done_skipped": "完了。{n} 件をスキップしました。",
|
||||
"cw_files_paste_halted": "停止しました。「{name}」は貼り付けられていません。置き換え対象の項目はまだ削除中です。",
|
||||
"cw_files_paste_here_already": "すでにこのフォルダーにあります。",
|
||||
"cw_files_path": "フォルダーのパス",
|
||||
"cw_files_path_more": "隠れているフォルダー {n} 個を表示",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"cw_files_conflict_body_self": "항목 자기 자신입니다. 건너뛰거나 둘 다 유지하세요(복사본에 새 이름을 붙임).",
|
||||
"cw_files_conflict_keep": "둘 다 유지",
|
||||
"cw_files_conflict_replace": "바꾸기",
|
||||
"cw_files_conflict_replace_all_body": "기존 파일은 덮어쓰고, 폴더는 그 안의 모든 것과 함께 먼저 삭제됩니다. 되돌릴 수 없습니다.",
|
||||
"cw_files_conflict_replace_all_title": "“{name}” 및 이후의 모든 충돌을 바꿀까요?",
|
||||
"cw_files_conflict_replace_dir_body": "기존 항목과 그 안의 모든 것이 먼저 삭제됩니다. 되돌릴 수 없습니다.",
|
||||
"cw_files_conflict_replace_file_body": "기존 파일을 덮어씁니다. 되돌릴 수 없습니다.",
|
||||
@@ -135,6 +136,7 @@
|
||||
"cw_files_order_asc": "오름차순",
|
||||
"cw_files_order_desc": "내림차순",
|
||||
"cw_files_paste_done_skipped": "완료. {n}개 건너뜀.",
|
||||
"cw_files_paste_halted": "중지했습니다. “{name}”은(는) 붙여넣지 않았습니다. 바꿀 항목은 아직 삭제 중입니다.",
|
||||
"cw_files_paste_here_already": "이미 이 폴더에 있습니다.",
|
||||
"cw_files_path": "폴더 경로",
|
||||
"cw_files_path_more": "숨겨진 폴더 {n}개 표시",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"cw_files_conflict_body_self": "To jest ten sam element. Pomiń go albo zachowaj oba (kopia dostanie nową nazwę).",
|
||||
"cw_files_conflict_keep": "Zachowaj oba",
|
||||
"cw_files_conflict_replace": "Zastąp",
|
||||
"cw_files_conflict_replace_all_body": "Pliki, które już tu są, zostaną nadpisane; foldery wraz z całą zawartością zostaną najpierw usunięte. Tego nie da się cofnąć.",
|
||||
"cw_files_conflict_replace_all_title": "Zastąpić „{name}” i wszystkie kolejne konflikty?",
|
||||
"cw_files_conflict_replace_dir_body": "To, co już tu jest, wraz z całą zawartością, zostanie najpierw usunięte. Tego nie da się cofnąć.",
|
||||
"cw_files_conflict_replace_file_body": "Plik, który już tu jest, zostanie nadpisany. Tego nie da się cofnąć.",
|
||||
@@ -135,6 +136,7 @@
|
||||
"cw_files_order_asc": "rosnąco",
|
||||
"cw_files_order_desc": "malejąco",
|
||||
"cw_files_paste_done_skipped": "Gotowe. Pominięto: {n}.",
|
||||
"cw_files_paste_halted": "Zatrzymano. „{name}” nie został wklejony; element, który miał zastąpić, jest jeszcze usuwany.",
|
||||
"cw_files_paste_here_already": "Już jest w tym folderze.",
|
||||
"cw_files_path": "Ścieżka folderu",
|
||||
"cw_files_path_more": "Pokaż ukryte foldery ({n})",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"cw_files_conflict_body_self": "Este é o próprio item. Pule ou mantenha os dois (a cópia ganha um novo nome).",
|
||||
"cw_files_conflict_keep": "Manter os dois",
|
||||
"cw_files_conflict_replace": "Substituir",
|
||||
"cw_files_conflict_replace_all_body": "Os arquivos que já estão aqui são sobrescritos; as pastas, com todo o conteúdo, são excluídas primeiro. Não dá para desfazer.",
|
||||
"cw_files_conflict_replace_all_title": "Substituir “{name}” e todos os conflitos seguintes?",
|
||||
"cw_files_conflict_replace_dir_body": "O que já está aqui, com todo o conteúdo, é excluído primeiro. Não dá para desfazer.",
|
||||
"cw_files_conflict_replace_file_body": "O arquivo que já está aqui é sobrescrito. Não dá para desfazer.",
|
||||
@@ -135,6 +136,7 @@
|
||||
"cw_files_order_asc": "crescente",
|
||||
"cw_files_order_desc": "decrescente",
|
||||
"cw_files_paste_done_skipped": "Pronto. {n} pulados.",
|
||||
"cw_files_paste_halted": "Parado. “{name}” não foi colado; o item que ele substituiria ainda está sendo excluído.",
|
||||
"cw_files_paste_here_already": "Já está nesta pasta.",
|
||||
"cw_files_path": "Caminho da pasta",
|
||||
"cw_files_path_more": "Mostrar {n} pastas ocultas",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"cw_files_conflict_body_self": "Это тот же самый элемент. Пропустите его или оставьте оба (копия получит новое имя).",
|
||||
"cw_files_conflict_keep": "Оставить оба",
|
||||
"cw_files_conflict_replace": "Заменить",
|
||||
"cw_files_conflict_replace_all_body": "Файлы, которые уже здесь, будут перезаписаны; папки вместе со всем содержимым сначала удаляются. Это нельзя отменить.",
|
||||
"cw_files_conflict_replace_all_title": "Заменить «{name}» и все следующие конфликты?",
|
||||
"cw_files_conflict_replace_dir_body": "То, что уже здесь, вместе со всем содержимым, сначала удаляется. Это нельзя отменить.",
|
||||
"cw_files_conflict_replace_file_body": "Файл, который уже здесь, будет перезаписан. Это нельзя отменить.",
|
||||
@@ -135,6 +136,7 @@
|
||||
"cw_files_order_asc": "по возрастанию",
|
||||
"cw_files_order_desc": "по убыванию",
|
||||
"cw_files_paste_done_skipped": "Готово. Пропущено: {n}.",
|
||||
"cw_files_paste_halted": "Остановлено. «{name}» не вставлен; заменяемый элемент ещё удаляется.",
|
||||
"cw_files_paste_here_already": "Уже в этой папке.",
|
||||
"cw_files_path": "Путь к папке",
|
||||
"cw_files_path_more": "Показать скрытые папки: {n}",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"cw_files_conflict_body_self": "นี่คือรายการเดียวกัน ข้ามไป หรือเก็บทั้งคู่ (สำเนาจะได้ชื่อใหม่)",
|
||||
"cw_files_conflict_keep": "เก็บทั้งคู่",
|
||||
"cw_files_conflict_replace": "แทนที่",
|
||||
"cw_files_conflict_replace_all_body": "ไฟล์ที่มีอยู่จะถูกเขียนทับ ส่วนโฟลเดอร์พร้อมทุกอย่างข้างในจะถูกลบก่อน ย้อนกลับไม่ได้",
|
||||
"cw_files_conflict_replace_all_title": "แทนที่ “{name}” และข้อขัดแย้งถัดไปทั้งหมดไหม",
|
||||
"cw_files_conflict_replace_dir_body": "รายการที่มีอยู่พร้อมทุกอย่างข้างในจะถูกลบก่อน ย้อนกลับไม่ได้",
|
||||
"cw_files_conflict_replace_file_body": "ไฟล์ที่มีอยู่จะถูกเขียนทับ ย้อนกลับไม่ได้",
|
||||
@@ -135,6 +136,7 @@
|
||||
"cw_files_order_asc": "น้อยไปมาก",
|
||||
"cw_files_order_desc": "มากไปน้อย",
|
||||
"cw_files_paste_done_skipped": "เสร็จแล้ว ข้าม {n} รายการ",
|
||||
"cw_files_paste_halted": "หยุดแล้ว ไม่ได้วาง “{name}” รายการที่จะถูกแทนที่ยังลบอยู่",
|
||||
"cw_files_paste_here_already": "อยู่ในโฟลเดอร์นี้แล้ว",
|
||||
"cw_files_path": "พาธของโฟลเดอร์",
|
||||
"cw_files_path_more": "แสดงโฟลเดอร์ที่ซ่อนอยู่ {n} โฟลเดอร์",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"cw_files_conflict_body_self": "Bu, öğenin kendisi. Atla ya da ikisini de tut (kopyaya yeni ad verilir).",
|
||||
"cw_files_conflict_keep": "İkisini de tut",
|
||||
"cw_files_conflict_replace": "Değiştir",
|
||||
"cw_files_conflict_replace_all_body": "Buradaki dosyaların üzerine yazılır; klasörler içindeki her şeyle birlikte önce silinir. Bu geri alınamaz.",
|
||||
"cw_files_conflict_replace_all_title": "“{name}” ve sonraki tüm çakışmalar değiştirilsin mi?",
|
||||
"cw_files_conflict_replace_dir_body": "Burada olan, içindeki her şeyle birlikte önce silinir. Bu geri alınamaz.",
|
||||
"cw_files_conflict_replace_file_body": "Buradaki dosyanın üzerine yazılır. Bu geri alınamaz.",
|
||||
@@ -135,6 +136,7 @@
|
||||
"cw_files_order_asc": "artan",
|
||||
"cw_files_order_desc": "azalan",
|
||||
"cw_files_paste_done_skipped": "Bitti. {n} atlandı.",
|
||||
"cw_files_paste_halted": "Durduruldu. “{name}” yapıştırılmadı; yerine geçeceği öğe hâlâ siliniyor.",
|
||||
"cw_files_paste_here_already": "Zaten bu klasörde.",
|
||||
"cw_files_path": "Klasör yolu",
|
||||
"cw_files_path_more": "{n} gizli klasörü göster",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"cw_files_conflict_body_self": "Đây chính là mục đó. Bỏ qua hoặc giữ cả hai (bản sao sẽ có tên mới).",
|
||||
"cw_files_conflict_keep": "Giữ cả hai",
|
||||
"cw_files_conflict_replace": "Thay thế",
|
||||
"cw_files_conflict_replace_all_body": "Các tệp đã có ở đây sẽ bị ghi đè; thư mục cùng toàn bộ nội dung sẽ bị xóa trước. Không thể hoàn tác.",
|
||||
"cw_files_conflict_replace_all_title": "Thay thế “{name}” và mọi xung đột sau đó?",
|
||||
"cw_files_conflict_replace_dir_body": "Mục đã có ở đây cùng toàn bộ nội dung sẽ bị xóa trước. Không thể hoàn tác.",
|
||||
"cw_files_conflict_replace_file_body": "Tệp đã có ở đây sẽ bị ghi đè. Không thể hoàn tác.",
|
||||
@@ -135,6 +136,7 @@
|
||||
"cw_files_order_asc": "tăng dần",
|
||||
"cw_files_order_desc": "giảm dần",
|
||||
"cw_files_paste_done_skipped": "Xong. Đã bỏ qua {n}.",
|
||||
"cw_files_paste_halted": "Đã dừng. “{name}” chưa được dán; mục cần thay thế vẫn đang bị xóa.",
|
||||
"cw_files_paste_here_already": "Đã ở trong thư mục này.",
|
||||
"cw_files_path": "Đường dẫn thư mục",
|
||||
"cw_files_path_more": "Hiện {n} thư mục bị ẩn",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"cw_files_conflict_body_self": "这就是该项目本身。可以跳过或保留两者(副本会改名)。",
|
||||
"cw_files_conflict_keep": "保留两者",
|
||||
"cw_files_conflict_replace": "替换",
|
||||
"cw_files_conflict_replace_all_body": "已有的文件会被覆盖;文件夹及其全部内容会先被删除。无法撤销。",
|
||||
"cw_files_conflict_replace_all_title": "替换“{name}”以及之后的所有冲突?",
|
||||
"cw_files_conflict_replace_dir_body": "已有的项目及其全部内容会先被删除,无法撤销。",
|
||||
"cw_files_conflict_replace_file_body": "已有的文件会被覆盖,无法撤销。",
|
||||
@@ -135,6 +136,7 @@
|
||||
"cw_files_order_asc": "升序",
|
||||
"cw_files_order_desc": "降序",
|
||||
"cw_files_paste_done_skipped": "完成。已跳过 {n} 个。",
|
||||
"cw_files_paste_halted": "已停止。“{name}”未粘贴;要被替换的项目仍在删除中。",
|
||||
"cw_files_paste_here_already": "已在此文件夹中。",
|
||||
"cw_files_path": "文件夹路径",
|
||||
"cw_files_path_more": "显示 {n} 个隐藏的文件夹",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"cw_files_conflict_body_self": "這就是該項目本身。可以略過或保留兩者(副本會改名)。",
|
||||
"cw_files_conflict_keep": "保留兩者",
|
||||
"cw_files_conflict_replace": "取代",
|
||||
"cw_files_conflict_replace_all_body": "現有的檔案會被覆寫;資料夾及其全部內容會先被刪除。無法復原。",
|
||||
"cw_files_conflict_replace_all_title": "取代「{name}」以及之後的所有衝突?",
|
||||
"cw_files_conflict_replace_dir_body": "現有的項目及其全部內容會先被刪除,無法復原。",
|
||||
"cw_files_conflict_replace_file_body": "現有的檔案會被覆寫,無法復原。",
|
||||
@@ -135,6 +136,7 @@
|
||||
"cw_files_order_asc": "遞增",
|
||||
"cw_files_order_desc": "遞減",
|
||||
"cw_files_paste_done_skipped": "完成。已略過 {n} 個。",
|
||||
"cw_files_paste_halted": "已停止。「{name}」未貼上;要被取代的項目仍在刪除中。",
|
||||
"cw_files_paste_here_already": "已在此資料夾中。",
|
||||
"cw_files_path": "資料夾路徑",
|
||||
"cw_files_path_more": "顯示 {n} 個隱藏的資料夾",
|
||||
|
||||
@@ -4346,6 +4346,8 @@ c2c_test_deep_bad: "لم يصل الإرسال التجريبي بحجم 4 KiB:
|
||||
"cw_files_paste_here_already": "موجود في هذا المجلد بالفعل.",
|
||||
"cw_files_paste_done_skipped": "تم. تخطي {n}.",
|
||||
"cw_files_replace_failed": "تعذّر حذف العنصر المراد استبداله، لذلك لم يُلصق.",
|
||||
"cw_files_conflict_replace_all_body": "يُكتب فوق الملفات الموجودة هنا؛ وتُحذف المجلدات بكل محتواها أولًا. لا يمكن التراجع عن ذلك.",
|
||||
"cw_files_paste_halted": "توقف. لم يُلصق «{name}»؛ ولا يزال العنصر الذي كان سيستبدله قيد الحذف.",
|
||||
};
|
||||
|
||||
export default ar;
|
||||
@@ -4346,6 +4346,8 @@ c2c_test_deep_bad: "4 KiB-এর পরীক্ষামূলক পাঠা
|
||||
"cw_files_paste_here_already": "আগে থেকেই এই ফোল্ডারে আছে।",
|
||||
"cw_files_paste_done_skipped": "হয়ে গেছে। {n}টি বাদ দেওয়া হয়েছে।",
|
||||
"cw_files_replace_failed": "প্রতিস্থাপনের আইটেমটি মোছা যায়নি, তাই পেস্ট করা হয়নি।",
|
||||
"cw_files_conflict_replace_all_body": "এখানে থাকা ফাইলগুলো ওভাররাইট হবে; ফোল্ডারগুলো ভেতরের সবকিছুসহ আগে মুছে ফেলা হবে। এটি ফেরানো যাবে না।",
|
||||
"cw_files_paste_halted": "থামানো হয়েছে। “{name}” পেস্ট হয়নি; যে আইটেমটি এটি প্রতিস্থাপন করত সেটি এখনও মোছা হচ্ছে।",
|
||||
};
|
||||
|
||||
export default bn;
|
||||
@@ -4354,6 +4354,8 @@ c2c_test_deep_bad: "Die 4-KiB-Testsendung ist nicht angekommen: {detail}",
|
||||
"cw_files_paste_here_already": "Schon in diesem Ordner.",
|
||||
"cw_files_paste_done_skipped": "Fertig. {n} übersprungen.",
|
||||
"cw_files_replace_failed": "Das zu ersetzende Element ließ sich nicht löschen, daher wurde nichts eingefügt.",
|
||||
"cw_files_conflict_replace_all_body": "Vorhandene Dateien werden überschrieben; Ordner werden samt Inhalt zuerst gelöscht. Das lässt sich nicht rückgängig machen.",
|
||||
"cw_files_paste_halted": "Gestoppt. „{name}“ wurde nicht eingefügt; das Element, das es ersetzen sollte, wird noch gelöscht.",
|
||||
};
|
||||
|
||||
export default de;
|
||||
@@ -4669,6 +4669,8 @@ c2c_test_deep_bad: "The 4 KiB test send did not arrive: {detail}",
|
||||
"cw_files_paste_here_already": "Already in this folder.",
|
||||
"cw_files_paste_done_skipped": "Done. {n} skipped.",
|
||||
"cw_files_replace_failed": "Couldn't delete the item being replaced, so it was not pasted.",
|
||||
"cw_files_conflict_replace_all_body": "Files already here are overwritten; folders, and everything in them, are deleted first. This can't be undone.",
|
||||
"cw_files_paste_halted": "Stopped. “{name}” was not pasted; the item it was replacing is still being deleted.",
|
||||
};
|
||||
|
||||
export default en;
|
||||
@@ -4296,6 +4296,8 @@ c2c_test_deep_bad: "El envío de prueba de 4 KiB no llegó: {detail}",
|
||||
"cw_files_paste_here_already": "Ya está en esta carpeta.",
|
||||
"cw_files_paste_done_skipped": "Listo. {n} omitidos.",
|
||||
"cw_files_replace_failed": "No se pudo eliminar el elemento a reemplazar, así que no se pegó.",
|
||||
"cw_files_conflict_replace_all_body": "Los archivos que ya están aquí se sobrescriben; las carpetas, con todo su contenido, se eliminan primero. No se puede deshacer.",
|
||||
"cw_files_paste_halted": "Detenido. «{name}» no se pegó; el elemento que iba a reemplazar todavía se está eliminando.",
|
||||
};
|
||||
|
||||
export default es;
|
||||
@@ -4647,6 +4647,8 @@ c2c_test_deep_bad: "ارسال آزمایشی ۴ KiB نرسید: {detail}",
|
||||
"cw_files_paste_here_already": "از قبل در همین پوشه است.",
|
||||
"cw_files_paste_done_skipped": "انجام شد. {n} مورد رد شد.",
|
||||
"cw_files_replace_failed": "حذف مورد جایگزینشونده ممکن نشد، پس چسبانده نشد.",
|
||||
"cw_files_conflict_replace_all_body": "فایلهای موجود بازنویسی میشوند؛ پوشهها با همه محتوایشان اول حذف میشوند. این کار برگشتپذیر نیست.",
|
||||
"cw_files_paste_halted": "متوقف شد. «{name}» چسبانده نشد؛ موردی که قرار بود جایگزینش شود هنوز در حال حذف است.",
|
||||
};
|
||||
|
||||
export default fa;
|
||||
@@ -4364,6 +4364,8 @@ c2c_test_deep_bad: "L’envoi test de 4 Kio n’est pas arrivé : {detail}",
|
||||
"cw_files_paste_here_already": "Déjà dans ce dossier.",
|
||||
"cw_files_paste_done_skipped": "Terminé. {n} ignorés.",
|
||||
"cw_files_replace_failed": "Impossible de supprimer l’élément à remplacer : rien n’a été collé.",
|
||||
"cw_files_conflict_replace_all_body": "Les fichiers déjà présents sont écrasés ; les dossiers, avec tout leur contenu, sont d’abord supprimés. Action irréversible.",
|
||||
"cw_files_paste_halted": "Arrêté. « {name} » n’a pas été collé ; l’élément qu’il devait remplacer est encore en cours de suppression.",
|
||||
};
|
||||
|
||||
export default fr;
|
||||
@@ -4362,6 +4362,8 @@ c2c_test_deep_bad: "4 KiB का परीक्षण नहीं पहुँ
|
||||
"cw_files_paste_here_already": "पहले से इसी फ़ोल्डर में है।",
|
||||
"cw_files_paste_done_skipped": "हो गया। {n} छोड़े गए।",
|
||||
"cw_files_replace_failed": "बदला जाने वाला आइटम हटाया नहीं जा सका, इसलिए पेस्ट नहीं किया गया।",
|
||||
"cw_files_conflict_replace_all_body": "यहाँ मौजूद फ़ाइलें ओवरराइट होंगी; फ़ोल्डर, उनके अंदर की हर चीज़ समेत, पहले हटा दिए जाएँगे। इसे पूर्ववत नहीं किया जा सकता।",
|
||||
"cw_files_paste_halted": "रोका गया। “{name}” पेस्ट नहीं हुआ; जिस आइटम को यह बदलने वाला था, वह अभी हटाया जा रहा है।",
|
||||
};
|
||||
|
||||
export default hi;
|
||||
@@ -4661,6 +4661,8 @@ c2c_test_deep_bad: "A 4 KiB-os tesztküldés nem érkezett meg: {detail}",
|
||||
"cw_files_paste_here_already": "Már ebben a mappában van.",
|
||||
"cw_files_paste_done_skipped": "Kész. {n} kihagyva.",
|
||||
"cw_files_replace_failed": "A lecserélendő elemet nem sikerült törölni, ezért nem lett beillesztve.",
|
||||
"cw_files_conflict_replace_all_body": "Az itt lévő fájlok felülíródnak; a mappák a teljes tartalmukkal együtt előbb törlődnek. Ez nem vonható vissza.",
|
||||
"cw_files_paste_halted": "Leállítva. „{name}” nem lett beillesztve; a lecserélendő elem törlése még folyamatban van.",
|
||||
};
|
||||
|
||||
export default hu;
|
||||
@@ -4355,6 +4355,8 @@ c2c_test_deep_bad: "Kiriman uji 4 KiB tidak tiba: {detail}",
|
||||
"cw_files_paste_here_already": "Sudah ada di folder ini.",
|
||||
"cw_files_paste_done_skipped": "Selesai. {n} dilewati.",
|
||||
"cw_files_replace_failed": "Item yang akan diganti tidak bisa dihapus, jadi tidak ditempel.",
|
||||
"cw_files_conflict_replace_all_body": "File yang sudah ada akan ditimpa; folder beserta seluruh isinya dihapus terlebih dahulu. Ini tidak bisa dibatalkan.",
|
||||
"cw_files_paste_halted": "Dihentikan. “{name}” tidak ditempel; item yang akan diganti masih sedang dihapus.",
|
||||
};
|
||||
|
||||
export default id;
|
||||
@@ -4296,6 +4296,8 @@ c2c_test_deep_bad: "L’invio di prova di 4 KiB non è arrivato: {detail}",
|
||||
"cw_files_paste_here_already": "Già in questa cartella.",
|
||||
"cw_files_paste_done_skipped": "Fatto. {n} saltati.",
|
||||
"cw_files_replace_failed": "Impossibile eliminare l’elemento da sostituire, quindi non è stato incollato.",
|
||||
"cw_files_conflict_replace_all_body": "I file già presenti vengono sovrascritti; le cartelle, con tutto il contenuto, vengono prima eliminate. Non si può annullare.",
|
||||
"cw_files_paste_halted": "Interrotto. «{name}» non è stato incollato; l’elemento da sostituire è ancora in fase di eliminazione.",
|
||||
};
|
||||
|
||||
export default it;
|
||||
@@ -4367,6 +4367,8 @@ c2c_test_deep_bad: "4 KiB のテスト送信が届きませんでした: {detail
|
||||
"cw_files_paste_here_already": "すでにこのフォルダーにあります。",
|
||||
"cw_files_paste_done_skipped": "完了。{n} 件をスキップしました。",
|
||||
"cw_files_replace_failed": "置き換える項目を削除できなかったため、貼り付けませんでした。",
|
||||
"cw_files_conflict_replace_all_body": "既存のファイルは上書きされ、フォルダーは中身ごと先に削除されます。元に戻せません。",
|
||||
"cw_files_paste_halted": "停止しました。「{name}」は貼り付けられていません。置き換え対象の項目はまだ削除中です。",
|
||||
};
|
||||
|
||||
export default ja;
|
||||
@@ -4365,6 +4365,8 @@ c2c_test_deep_bad: "4 KiB 테스트 전송이 도착하지 않았습니다: {det
|
||||
"cw_files_paste_here_already": "이미 이 폴더에 있습니다.",
|
||||
"cw_files_paste_done_skipped": "완료. {n}개 건너뜀.",
|
||||
"cw_files_replace_failed": "바꿀 항목을 삭제하지 못해 붙여넣지 않았습니다.",
|
||||
"cw_files_conflict_replace_all_body": "기존 파일은 덮어쓰고, 폴더는 그 안의 모든 것과 함께 먼저 삭제됩니다. 되돌릴 수 없습니다.",
|
||||
"cw_files_paste_halted": "중지했습니다. “{name}”은(는) 붙여넣지 않았습니다. 바꿀 항목은 아직 삭제 중입니다.",
|
||||
};
|
||||
|
||||
export default ko;
|
||||
@@ -4601,6 +4601,8 @@ c2c_test_deep_bad: "Testowa wysyłka 4 KiB nie dotarła: {detail}",
|
||||
"cw_files_paste_here_already": "Już jest w tym folderze.",
|
||||
"cw_files_paste_done_skipped": "Gotowe. Pominięto: {n}.",
|
||||
"cw_files_replace_failed": "Nie udało się usunąć zastępowanego elementu, więc nic nie wklejono.",
|
||||
"cw_files_conflict_replace_all_body": "Pliki, które już tu są, zostaną nadpisane; foldery wraz z całą zawartością zostaną najpierw usunięte. Tego nie da się cofnąć.",
|
||||
"cw_files_paste_halted": "Zatrzymano. „{name}” nie został wklejony; element, który miał zastąpić, jest jeszcze usuwany.",
|
||||
};
|
||||
|
||||
export default pl;
|
||||
@@ -4363,6 +4363,8 @@ c2c_test_deep_bad: "O envio de teste de 4 KiB não chegou: {detail}",
|
||||
"cw_files_paste_here_already": "Já está nesta pasta.",
|
||||
"cw_files_paste_done_skipped": "Pronto. {n} pulados.",
|
||||
"cw_files_replace_failed": "Não foi possível excluir o item a ser substituído, então ele não foi colado.",
|
||||
"cw_files_conflict_replace_all_body": "Os arquivos que já estão aqui são sobrescritos; as pastas, com todo o conteúdo, são excluídas primeiro. Não dá para desfazer.",
|
||||
"cw_files_paste_halted": "Parado. “{name}” não foi colado; o item que ele substituiria ainda está sendo excluído.",
|
||||
};
|
||||
|
||||
export default pt_BR;
|
||||
@@ -4296,6 +4296,8 @@ c2c_test_deep_bad: "Тестовая отправка 4 КиБ не дошла:
|
||||
"cw_files_paste_here_already": "Уже в этой папке.",
|
||||
"cw_files_paste_done_skipped": "Готово. Пропущено: {n}.",
|
||||
"cw_files_replace_failed": "Не удалось удалить заменяемый элемент, поэтому вставка не выполнена.",
|
||||
"cw_files_conflict_replace_all_body": "Файлы, которые уже здесь, будут перезаписаны; папки вместе со всем содержимым сначала удаляются. Это нельзя отменить.",
|
||||
"cw_files_paste_halted": "Остановлено. «{name}» не вставлен; заменяемый элемент ещё удаляется.",
|
||||
};
|
||||
|
||||
export default ru;
|
||||
@@ -4355,6 +4355,8 @@ c2c_test_deep_bad: "การส่งทดสอบ 4 KiB ไปไม่ถ
|
||||
"cw_files_paste_here_already": "อยู่ในโฟลเดอร์นี้แล้ว",
|
||||
"cw_files_paste_done_skipped": "เสร็จแล้ว ข้าม {n} รายการ",
|
||||
"cw_files_replace_failed": "ลบรายการที่จะแทนที่ไม่ได้ จึงไม่ได้วาง",
|
||||
"cw_files_conflict_replace_all_body": "ไฟล์ที่มีอยู่จะถูกเขียนทับ ส่วนโฟลเดอร์พร้อมทุกอย่างข้างในจะถูกลบก่อน ย้อนกลับไม่ได้",
|
||||
"cw_files_paste_halted": "หยุดแล้ว ไม่ได้วาง “{name}” รายการที่จะถูกแทนที่ยังลบอยู่",
|
||||
};
|
||||
|
||||
export default th;
|
||||
@@ -4353,6 +4353,8 @@ c2c_test_deep_bad: "4 KiB'lik test gönderimi ulaşmadı: {detail}",
|
||||
"cw_files_paste_here_already": "Zaten bu klasörde.",
|
||||
"cw_files_paste_done_skipped": "Bitti. {n} atlandı.",
|
||||
"cw_files_replace_failed": "Değiştirilecek öğe silinemedi, bu yüzden yapıştırılmadı.",
|
||||
"cw_files_conflict_replace_all_body": "Buradaki dosyaların üzerine yazılır; klasörler içindeki her şeyle birlikte önce silinir. Bu geri alınamaz.",
|
||||
"cw_files_paste_halted": "Durduruldu. “{name}” yapıştırılmadı; yerine geçeceği öğe hâlâ siliniyor.",
|
||||
};
|
||||
|
||||
export default tr;
|
||||
@@ -4348,6 +4348,8 @@ c2c_test_deep_bad: "Lượt gửi thử 4 KiB không tới: {detail}",
|
||||
"cw_files_paste_here_already": "Đã ở trong thư mục này.",
|
||||
"cw_files_paste_done_skipped": "Xong. Đã bỏ qua {n}.",
|
||||
"cw_files_replace_failed": "Không xóa được mục cần thay thế nên chưa dán.",
|
||||
"cw_files_conflict_replace_all_body": "Các tệp đã có ở đây sẽ bị ghi đè; thư mục cùng toàn bộ nội dung sẽ bị xóa trước. Không thể hoàn tác.",
|
||||
"cw_files_paste_halted": "Đã dừng. “{name}” chưa được dán; mục cần thay thế vẫn đang bị xóa.",
|
||||
};
|
||||
|
||||
export default vi;
|
||||
@@ -4302,6 +4302,8 @@ c2c_test_deep_bad: "4 KiB 测试发送未到达:{detail}",
|
||||
"cw_files_paste_here_already": "已在此文件夹中。",
|
||||
"cw_files_paste_done_skipped": "完成。已跳过 {n} 个。",
|
||||
"cw_files_replace_failed": "无法删除要替换的项目,因此未粘贴。",
|
||||
"cw_files_conflict_replace_all_body": "已有的文件会被覆盖;文件夹及其全部内容会先被删除。无法撤销。",
|
||||
"cw_files_paste_halted": "已停止。“{name}”未粘贴;要被替换的项目仍在删除中。",
|
||||
};
|
||||
|
||||
export default zh_CN;
|
||||
@@ -4296,6 +4296,8 @@ c2c_test_deep_bad: "4 KiB 測試傳送未抵達:{detail}",
|
||||
"cw_files_paste_here_already": "已在此資料夾中。",
|
||||
"cw_files_paste_done_skipped": "完成。已略過 {n} 個。",
|
||||
"cw_files_replace_failed": "無法刪除要取代的項目,因此未貼上。",
|
||||
"cw_files_conflict_replace_all_body": "現有的檔案會被覆寫;資料夾及其全部內容會先被刪除。無法復原。",
|
||||
"cw_files_paste_halted": "已停止。「{name}」未貼上;要被取代的項目仍在刪除中。",
|
||||
};
|
||||
|
||||
export default zh_TW;
|
||||
+19
-3
@@ -12,6 +12,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "ava1_data.h"
|
||||
@@ -347,9 +348,10 @@ static void r_fs_list(const char *b, size_t n, uint8_t *out, int fd) {
|
||||
}
|
||||
|
||||
static void r_fs_stat(const char *b, size_t n, uint8_t *out, int fd) {
|
||||
char path[API_PATH], req[API_PATH + 16], json[160];
|
||||
char path[API_PATH], req[API_PATH + 16], json[256];
|
||||
ava1_fs_path_t q;
|
||||
ava1_fs_stat_t s;
|
||||
struct stat ls;
|
||||
ava1_w_t w;
|
||||
size_t ol;
|
||||
int st, k;
|
||||
@@ -371,8 +373,22 @@ static void r_fs_stat(const char *b, size_t n, uint8_t *out, int fd) {
|
||||
send_error(fd, 500, "bad_stat_reply", 14);
|
||||
return;
|
||||
}
|
||||
k = snprintf(json, sizeof json, "{\"kind\":\"%s\",\"size\":%llu,\"mtime\":%llu,\"mode\":%lu}", kind_name(s.kind),
|
||||
(unsigned long long)s.size, (unsigned long long)s.mtime, (unsigned long)s.mode);
|
||||
/* The entry's identity (device + inode, as strings: an inode can pass 2^53) so the page can tell
|
||||
* "the item itself" from a different entry by more than its spelling: exFAT USB is
|
||||
* case-insensitive, so /mnt/usb0/Games and /mnt/usb0/games are one folder. lstat (the
|
||||
* entry itself, not a link's target) of the path the FS_STAT above already accepted. */
|
||||
if (lstat(path, &ls) == 0)
|
||||
k = snprintf(json, sizeof json,
|
||||
"{\"kind\":\"%s\",\"size\":%llu,\"mtime\":%llu,\"mode\":%lu,\"dev\":\"%llu\",\"ino\":\"%llu\"}",
|
||||
kind_name(s.kind), (unsigned long long)s.size, (unsigned long long)s.mtime, (unsigned long)s.mode,
|
||||
(unsigned long long)ls.st_dev, (unsigned long long)ls.st_ino);
|
||||
else
|
||||
k = snprintf(json, sizeof json, "{\"kind\":\"%s\",\"size\":%llu,\"mtime\":%llu,\"mode\":%lu}", kind_name(s.kind),
|
||||
(unsigned long long)s.size, (unsigned long long)s.mtime, (unsigned long)s.mode);
|
||||
if (k < 0 || (size_t)k >= sizeof json) {
|
||||
send_error(fd, 500, "bad_stat_reply", 14);
|
||||
return;
|
||||
}
|
||||
send_json(fd, 200, json, (size_t)k);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user