Files
NetMesh/application/state/sftp/transferConflictOps.ts

113 lines
4.1 KiB
TypeScript
Raw Normal View History

import { useCallback } from "react";
import type { SftpFilenameEncoding, TransferTask } from "../../../domain/models";
import { netcattyBridge } from "../../../infrastructure/services/netcattyBridge";
import { isMissingStatError } from "./errors";
import type { SftpPane } from "./types";
import { getParentPath, joinPath } from "./utils";
export function useSftpTransferConflictOps() {
const splitNameForDuplicate = useCallback((fileName: string, isDirectory: boolean) => {
if (isDirectory) return { baseName: fileName, ext: "" };
const lastDot = fileName.lastIndexOf(".");
if (lastDot <= 0) return { baseName: fileName, ext: "" };
return {
baseName: fileName.slice(0, lastDot),
ext: fileName.slice(lastDot),
};
}, []);
const statTargetPath = useCallback(
async (
targetPane: SftpPane,
targetSftpId: string | null,
targetPath: string,
targetEncoding: SftpFilenameEncoding,
): Promise<{ type?: "file" | "directory" | "symlink"; size: number; mtime: number } | null> => {
if (!targetPane.connection) return null;
try {
if (targetPane.connection.isLocal) {
const bridge = netcattyBridge.get();
const stat = await (bridge?.lstatLocal ?? bridge?.statLocal)?.(targetPath);
if (!stat) return null;
return {
type: stat.type as "file" | "directory" | "symlink" | undefined,
size: stat.size,
mtime: stat.lastModified || Date.now(),
};
}
if (!targetSftpId) return null;
const bridge = netcattyBridge.get();
const stat = await (bridge?.lstatSftp ?? bridge?.statSftp)?.(
targetSftpId,
targetPath,
targetEncoding,
);
if (!stat) return null;
return {
type: stat.type as "file" | "directory" | "symlink" | undefined,
size: stat.size,
mtime: stat.lastModified || Date.now(),
};
} catch (error) {
// Missing path = no conflict. ENOTSUP / unknown type must fail closed.
if (isMissingStatError(error)) return null;
throw error;
}
},
[],
);
const getDuplicateTarget = useCallback(
async (
task: TransferTask,
targetPane: SftpPane,
targetSftpId: string | null,
targetEncoding: SftpFilenameEncoding,
) => {
const parentPath = getParentPath(task.targetPath);
const { baseName, ext } = splitNameForDuplicate(task.fileName, task.isDirectory);
for (let index = 1; index < 1000; index++) {
const suffix = index === 1 ? " (copy)" : ` (copy ${index})`;
const fileName = `${baseName}${suffix}${ext}`;
const targetPath = joinPath(parentPath, fileName);
// Unsupported LSTAT must propagate — do not treat as a free name.
const existing = await statTargetPath(targetPane, targetSftpId, targetPath, targetEncoding);
if (!existing) return { fileName, targetPath };
}
const fallbackName = `${baseName} (copy ${Date.now()})${ext}`;
return { fileName: fallbackName, targetPath: joinPath(parentPath, fallbackName) };
},
[splitNameForDuplicate, statTargetPath],
);
const deleteTargetPath = useCallback(
async (
task: TransferTask,
targetPane: SftpPane,
targetSftpId: string | null,
targetEncoding: SftpFilenameEncoding,
expectedType?: "file" | "directory" | "symlink",
) => {
if (!targetPane.connection) return;
if (targetPane.connection.isLocal) {
const deleteLocalFile = netcattyBridge.get()?.deleteLocalFile;
if (!deleteLocalFile) throw new Error("Local delete unavailable");
await deleteLocalFile(task.targetPath, expectedType);
return;
}
if (!targetSftpId) throw new Error("Target SFTP session not found");
const deleteSftp = netcattyBridge.get()?.deleteSftp;
if (!deleteSftp) throw new Error("SFTP delete unavailable");
await deleteSftp(targetSftpId, task.targetPath, targetEncoding, expectedType);
},
[],
);
return { statTargetPath, getDuplicateTarget, deleteTargetPath };
}