113 lines
4.1 KiB
TypeScript
113 lines
4.1 KiB
TypeScript
|
|
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 };
|
||
|
|
}
|