Skip to content
24 changes: 14 additions & 10 deletions src/backend/drivers/139/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,18 +204,22 @@ export class Yun139Driver implements StorageDriver {
names: string[],
): Promise<void> {
const clean = this.cleanPath(physicalPath)
const catalogId = await this.resolveCatalogId(clean)
// physicalPath 是目标项自身的路径(op/storage.ts removeItems 逐项调用),
// 目标名取参数末段、父目录取去掉末段后的部分;把项路径当目录解析会走进
// 目标项内部(目录项尤其明显),相当于找 <item>/<name>,永远落空并静默
// 返回成功。
const name = clean.substring(clean.lastIndexOf("/") + 1)
const dirPath = clean.substring(0, clean.lastIndexOf("/")) || "/"
const catalogId = await this.resolveCatalogId(dirPath)
const disk = await this.client.listFiles(catalogId)

for (const name of names) {
const folder = disk.folders.find((f) => f.catalogName === name)
if (folder) {
await this.client.deleteCatalog(folder.catalogID)
} else {
const file = disk.files.find((f) => f.contentName === name)
if (file && file.contentID) {
await this.client.deleteFile(file.contentID)
}
const folder = disk.folders.find((f) => f.catalogName === name)
if (folder) {
await this.client.deleteCatalog(folder.catalogID)
} else {
const file = disk.files.find((f) => f.contentName === name)
if (file && file.contentID) {
await this.client.deleteFile(file.contentID)
}
}
}
Expand Down
28 changes: 23 additions & 5 deletions src/backend/drivers/alias/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ export class AliasDriver implements StorageDriver {
return s === "/" ? "/" : s
}

/** 取所在目录:physicalPath 是目标项自身路径时,去掉最后一段即其父目录 */
private parentPath(p: string): string {
const clean = this.cleanPath(p)
return clean.substring(0, clean.lastIndexOf("/")) || "/"
}

private parsePaths(): void {
const raw = this.addition.paths || ""
const lines = raw
Expand Down Expand Up @@ -231,7 +237,11 @@ export class AliasDriver implements StorageDriver {
physicalPath: string,
names: string[],
): Promise<void> {
const targets = this.getTargetsForPath(physicalPath)
// physicalPath 是目标项自身的路径(op/storage.ts removeItems 逐项调用),
// 而 removeItems 需要「父目录 + names」(内部会再拼 name);直接把项路径
// 当目录传入会得到 <item>/<name>,目标不存在导致删除静默失败。
const dirPath = this.parentPath(physicalPath)
const targets = this.getTargetsForPath(dirPath)
if (targets.length === 0) return
const { removeItems } = await import("../../internal/op/storage")
await removeItems(targets[0].targetFullPath, names)
Expand All @@ -244,8 +254,13 @@ export class AliasDriver implements StorageDriver {
srcPhys: string,
dstPhys: string,
): Promise<void> {
const srcTargets = this.getTargetsForPath(srcPhys)
const dstTargets = this.getTargetsForPath(dstPhys)
// srcPhys/dstPhys 是源/目标项自身的路径(已含 name),moveItems 需要的
// 是各自所在父目录(内部会再拼 name);传项路径会得到 <item>/<name>,
// 操作静默失败。
const srcDirPath = this.parentPath(srcPhys)
const dstDirPath = this.parentPath(dstPhys)
const srcTargets = this.getTargetsForPath(srcDirPath)
const dstTargets = this.getTargetsForPath(dstDirPath)
if (srcTargets.length === 0 || dstTargets.length === 0) {
throw new Error("[Alias] cannot resolve source or destination path")
}
Expand All @@ -264,8 +279,11 @@ export class AliasDriver implements StorageDriver {
srcPhys: string,
dstPhys: string,
): Promise<void> {
const srcTargets = this.getTargetsForPath(srcPhys)
const dstTargets = this.getTargetsForPath(dstPhys)
// 同 move:srcPhys/dstPhys 是项自身路径,copyItems 需要各自所在父目录。
const srcDirPath = this.parentPath(srcPhys)
const dstDirPath = this.parentPath(dstPhys)
const srcTargets = this.getTargetsForPath(srcDirPath)
const dstTargets = this.getTargetsForPath(dstDirPath)
if (srcTargets.length === 0 || dstTargets.length === 0) {
throw new Error("[Alias] cannot resolve source or destination path")
}
Expand Down
21 changes: 10 additions & 11 deletions src/backend/drivers/azure_blob/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,12 +302,12 @@ export class AzureBlobDriver implements StorageDriver {
srcPhys: string,
dstPhys: string,
): Promise<void> {
for (const name of names) {
const srcKey = joinPath(this.getKey(srcPhys), name)
const dstKey = joinPath(this.getKey(dstPhys), name)
await this.copyBlob(srcKey, dstKey)
await this.deleteBlob(srcKey)
}
// srcPhys/dstPhys 是目标项自身的物理路径(op/storage.ts moveItems 逐项调用),
// 不得再拼 name,否则会指向 `<item>/<name>` 这种不存在的 blob。
const srcKey = this.getKey(srcPhys)
const dstKey = this.getKey(dstPhys)
await this.copyBlob(srcKey, dstKey)
await this.deleteBlob(srcKey)
}

async copy(
Expand All @@ -317,11 +317,10 @@ export class AzureBlobDriver implements StorageDriver {
srcPhys: string,
dstPhys: string,
): Promise<void> {
for (const name of names) {
const srcKey = joinPath(this.getKey(srcPhys), name)
const dstKey = joinPath(this.getKey(dstPhys), name)
await this.copyBlob(srcKey, dstKey)
}
// 同 move:srcPhys/dstPhys 已是目标项自身路径,不得再拼 name。
const srcKey = this.getKey(srcPhys)
const dstKey = this.getKey(dstPhys)
await this.copyBlob(srcKey, dstKey)
}

async put(_v: string, physicalPath: string, content: Buffer): Promise<void> {
Expand Down
7 changes: 5 additions & 2 deletions src/backend/drivers/chunk/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,12 +239,14 @@ export class ChunkDriver implements StorageDriver {
await this.remoteDriver!.get("", srcRemote)
await this.remoteDriver!.move(srcDir, dstDir, names, srcRemote, dstRemote)
} catch {
// 分片文件:dst 端必须是目标项自己的分片目录(与虚拟文件同级),
// 拼成 `<目标项>/<chunkPrefix><name>` 会指向不存在的路径。
await this.remoteDriver!.move(
srcDir,
dstDir,
names,
this.chunkDirRemote(srcPhysical),
joinPath(dstRemote, this.chunkPrefix + basename(srcPhysical)),
this.chunkDirRemote(dstPhysical),
)
}
}
Expand All @@ -263,12 +265,13 @@ export class ChunkDriver implements StorageDriver {
await this.remoteDriver!.get("", srcRemote)
await this.remoteDriver!.copy(srcDir, dstDir, names, srcRemote, dstRemote)
} catch {
// 分片文件:同 move,dst 端用目标项自己的分片目录。
await this.remoteDriver!.copy(
srcDir,
dstDir,
names,
this.chunkDirRemote(srcPhysical),
joinPath(dstRemote, this.chunkPrefix + basename(srcPhysical)),
this.chunkDirRemote(dstPhysical),
)
}
}
Expand Down
33 changes: 14 additions & 19 deletions src/backend/drivers/dropbox/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,10 @@ export class DropboxDriver implements StorageDriver {
physicalPath: string,
names: string[],
): Promise<void> {
const cleanDir = this.cleanPath(physicalPath)
for (const name of names) {
const target = cleanDir === "" ? `/${name}` : `${cleanDir}/${name}`
await this.client.delete(target)
}
// physicalPath 是目标项自身的物理路径(op/storage.ts removeItems 逐项调用),
// 直接删除它即可;不得再拼 name,否则指向 `<item>/<name>`,delete 404 报错。
const target = this.cleanPath(physicalPath)
await this.client.delete(target)
}

async move(
Expand All @@ -141,13 +140,11 @@ export class DropboxDriver implements StorageDriver {
srcPhys: string,
dstPhys: string,
): Promise<void> {
const cleanSrc = this.cleanPath(srcPhys)
const cleanDst = this.cleanPath(dstPhys)
for (const name of names) {
const from = cleanSrc === "" ? `/${name}` : `${cleanSrc}/${name}`
const to = cleanDst === "" ? `/${name}` : `${cleanDst}/${name}`
await this.client.move(from, to)
}
// srcPhys/dstPhys 已是源/目标项自身的物理路径(op/storage.ts moveItems 逐项调用),
// 直接作为 from/to 使用;不得再拼 name,否则指向 `<item>/<name>`,源/目标错位。
const from = this.cleanPath(srcPhys)
const to = this.cleanPath(dstPhys)
await this.client.move(from, to)
}

async copy(
Expand All @@ -157,13 +154,11 @@ export class DropboxDriver implements StorageDriver {
srcPhys: string,
dstPhys: string,
): Promise<void> {
const cleanSrc = this.cleanPath(srcPhys)
const cleanDst = this.cleanPath(dstPhys)
for (const name of names) {
const from = cleanSrc === "" ? `/${name}` : `${cleanSrc}/${name}`
const to = cleanDst === "" ? `/${name}` : `${cleanDst}/${name}`
await this.client.copy(from, to)
}
// srcPhys/dstPhys 已是源/目标项自身的物理路径(op/storage.ts copyItems 逐项调用),
// 直接作为 from/to 使用;不得再拼 name,否则指向 `<item>/<name>`,源/目标错位。
const from = this.cleanPath(srcPhys)
const to = this.cleanPath(dstPhys)
await this.client.copy(from, to)
}

async put(
Expand Down
123 changes: 123 additions & 0 deletions src/backend/drivers/dropbox/dropbox_remove.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import assert from "node:assert/strict"
import { test } from "node:test"
import { DropboxDriver } from "./driver"

/**
* 调用约定(src/backend/internal/op/storage.ts 的 removeItems/moveItems):
*
* const resolved = await resolvePath(`${dir}/${name}`)
* await driver.remove(virtualPath, resolved.physical, [name])
*
* 即 physicalPath/srcPhys/dstPhys 是【目标项自身】的物理路径(已含文件名),
* 调用方恒传单元素 names。driver 不得再把 name 拼到该路径后面,否则 body.path
* 变成 `<item>/<name>`:delete_v2/move_v2 找不到该路径 → 404 报错,
* 目标项既没被删除也没被移动。
*/

type Call = { method: string; url: string; body: string }

/** 记录请求,恒返回 200 + application/json(util.request 走 res.json() 分支) */
function mockDropbox(calls: Call[]) {
return async (input: any, init: any = {}) => {
const url = String(typeof input === "string" ? input : input.url)
calls.push({
method: String(init.method || "GET").toUpperCase(),
url,
body: typeof init.body === "string" ? init.body : "",
})
return new Response("{}", {
status: 200,
headers: { "content-type": "application/json" },
})
}
}

function withMock<T>(calls: Call[], fn: () => Promise<T>): Promise<T> {
const original = globalThis.fetch
globalThis.fetch = mockDropbox(calls) as any
return fn().finally(() => {
globalThis.fetch = original
})
}

function makeDriver() {
// access_token 必填:util.request 发现无 token 会先联网 refreshToken(),
// 测试不调用 init(),也不允许出现 oauth2/token 请求。
return new DropboxDriver({
access_token: "tok",
refresh_token: "r",
client_id: "id",
client_secret: "sec",
})
}

test("Dropbox remove() 删除项自身路径,不再拼一次 name", async () => {
const calls: Call[] = []

await withMock(calls, () =>
makeDriver().remove("/d/a.txt", "/d/a.txt", ["a.txt"]),
)

const deletes = calls.filter(
(c) => c.url === "https://api.dropboxapi.com/2/files/delete_v2",
)
assert.equal(
deletes.length,
1,
`应恰好发出 1 次 files/delete_v2 请求,实际 ${deletes.length}`,
)
const body = JSON.parse(deletes[0].body)
assert.equal(
body.path,
"/d/a.txt",
`delete 的 path 应为项自身路径 /d/a.txt,实际 ${body.path}`,
)
assert.ok(
!String(body.path).endsWith("/a.txt/a.txt"),
"path 不得出现二次拼接的 <item>/<name>",
)
assert.ok(
calls.every((c) => !c.url.includes("/oauth2/token")),
"已带 access_token,不应触发 token 刷新请求",
)
})

test("Dropbox move() 的 from_path/to_path 分别为源/目标项自身路径", async () => {
const calls: Call[] = []

await withMock(calls, () =>
makeDriver().move("/d", "/d/dst", ["a.txt"], "/d/a.txt", "/d/dst/a.txt"),
)

const moves = calls.filter(
(c) => c.url === "https://api.dropboxapi.com/2/files/move_v2",
)
assert.equal(
moves.length,
1,
`应恰好发出 1 次 files/move_v2 请求,实际 ${moves.length}`,
)
const body = JSON.parse(moves[0].body)
assert.equal(
body.from_path,
"/d/a.txt",
`from_path 应为源项自身路径 /d/a.txt,实际 ${body.from_path}`,
)
assert.equal(
body.to_path,
"/d/dst/a.txt",
`to_path 应为目标项自身路径 /d/dst/a.txt,实际 ${body.to_path}`,
)
assert.ok(
!String(body.from_path).endsWith("/a.txt/a.txt"),
"from_path 不得出现二次拼接的 <item>/<name>",
)
assert.ok(
!String(body.to_path).endsWith("/a.txt/a.txt"),
"to_path 不得出现二次拼接的 <item>/<name>",
)
assert.ok(
calls.every((c) => !c.url.includes("/oauth2/token")),
"已带 access_token,不应触发 token 刷新请求",
)
})
30 changes: 10 additions & 20 deletions src/backend/drivers/ftp/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,34 +140,24 @@ export class FTPDriver implements StorageDriver {
physicalPath: string,
names: string[],
): Promise<void> {
const targetDir = cleanPosixPath(physicalPath)
if (names && names.length > 0) {
for (const name of names) {
await this.client.removeRecursive(posixJoin(targetDir, name))
}
} else {
await this.client.removeRecursive(targetDir)
}
// physicalPath 已是目标项自身的物理路径(op/storage.ts 逐项解析后传入),
// 直接删除即可;不得再拼 name,否则指向不存在的 <item>/<name>,导致静默删除失败。
const target = cleanPosixPath(physicalPath)
await this.client.removeRecursive(target)
}

async move(
_srcDir: string,
dstDir: string,
_dstDir: string,
names: string[],
srcPhys: string,
dstPhys: string,
): Promise<void> {
if (names && names.length > 0) {
for (const name of names) {
const src = posixJoin(srcPhys, name)
const dst = posixJoin(dstPhys, name)
await this.client.rename(src, dst)
}
} else {
const filename = srcPhys.split("/").filter(Boolean).pop() || ""
const dst = posixJoin(dstDir, filename)
await this.client.rename(cleanPosixPath(srcPhys), dst)
}
// srcPhys/dstPhys 已是源/目标项自身的物理路径,直接重命名即可;
// 不得再拼 name,否则源/目标路径错位。
const src = cleanPosixPath(srcPhys)
const dst = cleanPosixPath(dstPhys)
await this.client.rename(src, dst)
}

async copy(
Expand Down
Loading