Skip to content

Commit

Permalink
Merge pull request #361 from susnux/enh/allow-depth-and-overwrite-cop…
Browse files Browse the repository at this point in the history
…y-move

enh: Allow to set `overwrite` for copy and move actions and allow shallow copy
  • Loading branch information
perry-mitchell committed Mar 18, 2024
2 parents c446c66 + a379d98 commit cbf2f7a
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 11 deletions.
6 changes: 4 additions & 2 deletions source/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { getFileUploadLink, putFileContents } from "./operations/putFileContents
import {
AuthType,
BufferLike,
CopyFileOptions,
CreateReadStreamOptions,
CreateWriteStreamCallback,
CreateWriteStreamOptions,
Expand All @@ -26,6 +27,7 @@ import {
GetQuotaOptions,
Headers,
LockOptions,
MoveFileOptions,
PutFileContentsOptions,
RequestOptionsCustom,
SearchOptions,
Expand Down Expand Up @@ -74,7 +76,7 @@ export function createClient(remoteURL: string, options: WebDAVClientOptions = {
};
setupAuth(context, username, password, token, ha1);
return {
copyFile: (filename: string, destination: string, options?: WebDAVMethodOptions) =>
copyFile: (filename: string, destination: string, options?: CopyFileOptions) =>
copyFile(context, filename, destination, options),
createDirectory: (path: string, options?: WebDAVMethodOptions) =>
createDirectory(context, path, options),
Expand All @@ -99,7 +101,7 @@ export function createClient(remoteURL: string, options: WebDAVClientOptions = {
getHeaders: () => Object.assign({}, context.headers),
getQuota: (options?: GetQuotaOptions) => getQuota(context, options),
lock: (path: string, options?: LockOptions) => lock(context, path, options),
moveFile: (filename: string, destinationFilename: string, options?: WebDAVMethodOptions) =>
moveFile: (filename: string, destinationFilename: string, options?: MoveFileOptions) =>
moveFile(context, filename, destinationFilename, options),
putFileContents: (
filename: string,
Expand Down
19 changes: 16 additions & 3 deletions source/operations/copyFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,33 @@ import { joinURL } from "../tools/url.js";
import { encodePath } from "../tools/path.js";
import { request, prepareRequestOptions } from "../request.js";
import { handleResponseCode } from "../response.js";
import { WebDAVClientContext, WebDAVMethodOptions } from "../types.js";
import { CopyFileOptions, WebDAVClientContext, WebDAVMethodOptions } from "../types.js";

export async function copyFile(
context: WebDAVClientContext,
filename: string,
destination: string,
options: WebDAVMethodOptions = {}
options: CopyFileOptions = {}
): Promise<void> {
const requestOptions = prepareRequestOptions(
{
url: joinURL(context.remoteURL, encodePath(filename)),
method: "COPY",
headers: {
Destination: joinURL(context.remoteURL, encodePath(destination))
Destination: joinURL(context.remoteURL, encodePath(destination)),
/**
* From RFC4918 section 10.6: If the overwrite header is not included in a COPY or MOVE request,
* then the resource MUST treat the request as if it has an overwrite header of value "T".
*
* Meaning the overwrite header is always set to "T" EXCEPT the option is explicitly set to false.
*/
Overwrite: options.overwrite === false ? "F" : "T",
/**
* From RFC4918 section 9.8.3: A client may submit a Depth header on a COPY on a collection with a value of "0"
* or "infinity". The COPY method on a collection without a Depth header MUST act as if
* a Depth header with value "infinity" was included.
*/
Depth: options.shallow ? "0" : "infinity"
}
},
context,
Expand Down
13 changes: 10 additions & 3 deletions source/operations/moveFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,27 @@ import { joinURL } from "../tools/url.js";
import { encodePath } from "../tools/path.js";
import { request, prepareRequestOptions } from "../request.js";
import { handleResponseCode } from "../response.js";
import { WebDAVClientContext, WebDAVMethodOptions } from "../types.js";
import { MoveFileOptions, WebDAVClientContext, WebDAVMethodOptions } from "../types.js";

export async function moveFile(
context: WebDAVClientContext,
filename: string,
destination: string,
options: WebDAVMethodOptions = {}
options: MoveFileOptions = {}
): Promise<void> {
const requestOptions = prepareRequestOptions(
{
url: joinURL(context.remoteURL, encodePath(filename)),
method: "MOVE",
headers: {
Destination: joinURL(context.remoteURL, encodePath(destination))
Destination: joinURL(context.remoteURL, encodePath(destination)),
/**
* From RFC4918 section 10.6: If the overwrite header is not included in a COPY or MOVE request,
* then the resource MUST treat the request as if it has an overwrite header of value "T".
*
* Meaning the overwrite header is always set to "T" EXCEPT the option is explicitly set to false.
*/
Overwrite: options.overwrite === false ? "F" : "T"
}
},
context,
Expand Down
25 changes: 23 additions & 2 deletions source/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,23 @@ export interface GetQuotaOptions extends WebDAVMethodOptions {
path?: string;
}

export interface MoveFileOptions extends WebDAVMethodOptions {
/**
* Set to false to disable overwriting an existing resource during MOVE or COPY.
* @default true
*/
overwrite?: boolean;
}

export interface CopyFileOptions extends MoveFileOptions {
/**
* Set to true to create a shallow copy only by only copying the resource with its properties but without its children.
* By default the collection itself with all children is copied (WebDAV Depth header of 'infinity')
* @default false
*/
shallow?: boolean;
}

export interface Headers {
[key: string]: string;
}
Expand Down Expand Up @@ -237,7 +254,7 @@ export type UploadProgress = ProgressEvent;
export type UploadProgressCallback = ProgressEventCallback;

export interface WebDAVClient {
copyFile: (filename: string, destination: string) => Promise<void>;
copyFile: (filename: string, destination: string, options?: CopyFileOptions) => Promise<void>;
createDirectory: (path: string, options?: CreateDirectoryOptions) => Promise<void>;
createReadStream: (filename: string, options?: CreateReadStreamOptions) => Stream.Readable;
createWriteStream: (
Expand All @@ -263,7 +280,11 @@ export interface WebDAVClient {
options?: GetQuotaOptions
) => Promise<DiskQuota | null | ResponseDataDetailed<DiskQuota | null>>;
lock: (path: string, options?: LockOptions) => Promise<LockResponse>;
moveFile: (filename: string, destinationFilename: string) => Promise<void>;
moveFile: (
filename: string,
destinationFilename: string,
options?: MoveFileOptions
) => Promise<void>;
putFileContents: (
filename: string,
data: string | BufferLike | Stream.Readable,
Expand Down
18 changes: 18 additions & 0 deletions test/node/operations/copyFile.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,22 @@ describe("copyFile", function () {
const [, requestOptions] = this.requestSpy.firstCall.args;
expect(requestOptions).to.have.property("headers").that.has.property("X-test", "test");
});

it("creates deep copy by default", async function () {
await this.client.copyFile("/alrighty.jpg", "/sub1/alrighty.jpg");
const [, requestOptions] = this.requestSpy.firstCall.args;
expect(requestOptions).to.have.property("headers").that.has.property("Depth", "infinity");
});

it("creates deep copy if shallow copy is disabled", async function () {
await this.client.copyFile("/alrighty.jpg", "/sub1/alrighty.jpg", { shallow: false });
const [, requestOptions] = this.requestSpy.firstCall.args;
expect(requestOptions).to.have.property("headers").that.has.property("Depth", "infinity");
});

it("creates shallow copy if enabled", async function () {
await this.client.copyFile("/alrighty.jpg", "/sub1/alrighty.jpg", { shallow: true });
const [, requestOptions] = this.requestSpy.firstCall.args;
expect(requestOptions).to.have.property("headers").that.has.property("Depth", "0");
});
});
38 changes: 37 additions & 1 deletion test/node/operations/moveFile.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
SERVER_USERNAME,
clean,
createWebDAVClient,
createWebDAVServer
createWebDAVServer,
restoreRequests,
useRequestSpy
} from "../../helpers.node.js";

const dirname = path.dirname(fileURLToPath(import.meta.url));
Expand All @@ -24,10 +26,12 @@ describe("moveFile", function () {
});
clean();
this.server = createWebDAVServer();
this.requestSpy = useRequestSpy();
return this.server.start();
});

afterEach(function () {
restoreRequests();
return this.server.stop();
});

Expand All @@ -51,4 +55,36 @@ describe("moveFile", function () {
expect(fileExists.sync(path.join(TEST_CONTENTS, "./renamed.jpg"))).to.be.true;
});
});

it("Overwrite on move by default", async function () {
await this.client.moveFile("/two words/file.txt", "/with & in path/files.txt");
const [, requestOptions] = this.requestSpy.firstCall.args;
expect(requestOptions).to.have.property("headers").that.has.property("Overwrite", "T");
});

it("Overwrite on move if explicitly enabled", async function () {
await this.client.moveFile("/two words/file.txt", "/with & in path/files.txt", {
overwrite: true
});
const [, requestOptions] = this.requestSpy.firstCall.args;
expect(requestOptions).to.have.property("headers").that.has.property("Overwrite", "T");
});

it("Do not overwrite if disabled", async function () {
try {
await this.client.moveFile("/two words/file.txt", "/with & in path/files.txt", {
overwrite: false
});
} catch (e) {
expect(e).to.have.property("status");
expect(e.status).to.equal(412);
return;
} finally {
const [, requestOptions] = this.requestSpy.firstCall.args;
expect(requestOptions).to.have.property("headers").that.has.property("Overwrite", "F");
}

// should not happen (reach this) but the webserver implementation is not following RFC
// expect("Move file should not work!").to.be.true
});
});

0 comments on commit cbf2f7a

Please sign in to comment.