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:
phantomptrandClaude Opus 5.5 committed 2026-10-17 12:49:19 -07:00
1 parent 910321cb4a
commit dccc77f38b
49 files changed
+320 -13

No files matched your search

+7 -1
View File
@@ -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 })}
+8 -1
View File
@@ -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 })}`,
);
});
});
+15 -1
View File
@@ -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}
+11
View File
@@ -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();
}
+112
View File
@@ -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([]);
});
});
+64 -7
View File
@@ -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,
};
+2
View File
@@ -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})",
+2
View File
@@ -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}টি ফোল্ডার দেখান",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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} پوشهٔ پنهان",
+2
View File
@@ -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",
+2
View File
@@ -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} छिपे फ़ोल्डर दिखाएँ",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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} 個を表示",
+2
View File
@@ -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}개 표시",
+2
View File
@@ -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})",
+2
View File
@@ -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",
+2
View File
@@ -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}",
+2
View File
@@ -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} โฟลเดอร์",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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} 个隐藏的文件夹",
+2
View File
@@ -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} 個隱藏的資料夾",
+2
View File
@@ -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;
+2
View File
@@ -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;
+2
View File
@@ -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;
+2
View File
@@ -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;
+2
View File
@@ -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;
+2
View File
@@ -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;
+2
View File
@@ -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;
+2
View File
@@ -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;
+2
View File
@@ -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;
+2
View File
@@ -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;
+2
View File
@@ -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;
+2
View File
@@ -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;
+2
View File
@@ -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;
+2
View File
@@ -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;
+2
View File
@@ -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;
+2
View File
@@ -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;
+2
View File
@@ -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;
+2
View File
@@ -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;
+2
View File
@@ -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;
+2
View File
@@ -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;
+2
View File
@@ -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
View File
@@ -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);
}