Skip to content

Commit

Permalink
Added support for copyObject
Browse files Browse the repository at this point in the history
  • Loading branch information
lucacasonato committed Jul 27, 2020
1 parent 7fa38dd commit fa75e7a
Show file tree
Hide file tree
Showing 4 changed files with 245 additions and 2 deletions.
5 changes: 5 additions & 0 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
export * from "./src/bucket.ts";
export type {
GetObjectOptions,
GetObjectResponse,
PutObjectOptions,
PutObjectResponse,
CopyObjectOptions,
CopyObjectResponse,
DeleteObjectOptions,
DeleteObjectResponse,
} from "./src/types.ts";
96 changes: 96 additions & 0 deletions src/bucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
LockMode,
ReplicationStatus,
StorageClass,
CopyObjectOptions,
} from "./types.ts";
import { S3Error } from "./error.ts";

Expand Down Expand Up @@ -209,6 +210,7 @@ export class S3Bucket {
? "ON"
: "OFF";
}

const resp = await this._doRequest(
key,
{},
Expand All @@ -228,6 +230,100 @@ export class S3Bucket {
};
}

async copyObject(
source: string,
destination: string,
options?: CopyObjectOptions,
): Promise<PutObjectResponse> {
const headers: Params = {};
headers["x-amz-copy-source"] = new URL(encodeURIS3(source), this.#host)
.toString();
if (options?.acl) headers["x-amz-acl"] = options.acl;
if (options?.cacheControl) headers["Cache-Control"] = options.cacheControl;
if (options?.contentDisposition) {
headers["Content-Disposition"] = options.contentDisposition;
}
if (options?.contentEncoding) {
headers["Content-Encoding"] = options.contentEncoding;
}
if (options?.contentLanguage) {
headers["Content-Language"] = options.contentLanguage;
}
if (options?.contentType) headers["Content-Type"] = options.contentType;
if (options?.copyOnlyIfMatch) {
headers["x-amz-copy-source-if-match"] = options.copyOnlyIfMatch;
}
if (options?.copyOnlyIfNoneMatch) {
headers["x-amz-copy-source-if-none-match"] = options.copyOnlyIfNoneMatch;
}
if (options?.copyOnlyIfModifiedSince) {
headers["x-amz-copy-source-if-modified-since"] = options
.copyOnlyIfModifiedSince
.toISOString();
}
if (options?.copyOnlyIfUnmodifiedSince) {
headers["x-amz-copy-source-if-unmodified-since"] = options
.copyOnlyIfUnmodifiedSince
.toISOString();
}
if (options?.grantFullControl) {
headers["x-amz-grant-full-control"] = options.grantFullControl;
}
if (options?.grantRead) headers["x-amz-grant-read"] = options.grantRead;
if (options?.grantReadAcp) {
headers["x-amz-grant-read-acp"] = options.grantReadAcp;
}
if (options?.grantWriteAcp) {
headers["x-amz-grant-write-acp"] = options.grantWriteAcp;
}
if (options?.storageClass) {
headers["x-amz-storage-class"] = options.storageClass;
}

if (options?.websiteRedirectLocation) {
headers["x-amz-website-redirect-location"] =
options.websiteRedirectLocation;
}
if (options?.tags) {
const p = new URLSearchParams(options.tags);
headers["x-amz-tagging"] = p.toString();
}
if (options?.lockMode) headers["x-amz-object-lock-mode"] = options.lockMode;
if (options?.lockRetainUntil) {
headers["x-amz-object-lock-retain-until-date"] = options.lockRetainUntil
.toString();
}
if (options?.legalHold) {
headers["x-amz-object-lock-legal-hold"] = options.legalHold
? "ON"
: "OFF";
}
if (options?.metadataDirective) {
headers["x-amz-metadata-directive"] = options.metadataDirective;
}
if (options?.taggingDirective) {
headers["x-amz-tagging-directive"] = options.taggingDirective;
}

const resp = await this._doRequest(
destination,
{},
"PUT",
headers,
);
if (resp.status !== 200) {
throw new S3Error(
`Failed to copy object: ${resp.status} ${resp.statusText}`,
await resp.text(),
);
}
await resp.arrayBuffer();
return {
etag: JSON.parse(resp.headers.get("etag")!),
versionId: resp.headers.get("x-amz-version-id") ?? undefined,
};
}

async deleteObject(
key: string,
options?: DeleteObjectOptions,
Expand Down
22 changes: 20 additions & 2 deletions src/bucket_test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { assert, assertEquals, assertThrowsAsync } from "../test_deps.ts";
import { assert, assertEquals } from "../test_deps.ts";
import { S3Bucket } from "./bucket.ts";
import { S3Error } from "./error.ts";

const bucket = new S3Bucket({
accessKeyID: Deno.env.get("AWS_ACCESS_KEY_ID")!,
Expand Down Expand Up @@ -90,3 +89,22 @@ Deno.test({
assertEquals(await bucket.getObject("test"), undefined);
},
});

Deno.test({
name: "copy object",
async fn() {
await bucket.putObject(
"test3",
encoder.encode("Test1"),
);
await bucket.copyObject("test3", "test4", {
contentType: "text/plain",
metadataDirective: "REPLACE",
}).catch((e) => console.log(e.response));
const res = await bucket.getObject("test4");
assert(res);
assertEquals(res.contentType, "text/plain");
assertEquals(res.contentLength, 5);
assertEquals(res.contentType, "text/plain");
},
});
124 changes: 124 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export type StorageClass =
| "GLACIER"
| "DEEP_ARCHIVE";

export type CopyDirective = "COPY" | "REPLACE";

export interface GetObjectOptions {
/**
* Return the object only if its entity tag (ETag) is the same as the one
Expand Down Expand Up @@ -257,6 +259,128 @@ export interface PutObjectResponse {
versionId?: string;
}

export interface CopyObjectOptions {
acl?:
| "private"
| "public-read"
| "public-read-write"
| "authenticated-read"
| "aws-exec-read"
| "bucket-owner-read"
| "bucket-owner-full-control";

/** Can be used to specify caching behavior along the request/reply chain. */
cacheControl?: string;

/** Specifies presentational information for the object. */
contentDisposition?: string;

/**
* Specifies what content encodings have been applied to the object
* and thus what decoding mechanisms must be applied to obtain the
* media-type referenced by the Content-Type field.
*/
contentEncoding?: string;

/** The language the content is in. */
contentLanguage?: string;

/** A standard MIME type describing the format of the object data. */
contentType?: string;

/** The date and time at which the object is no longer cacheable. */
expires?: Date;

/**
* Copy the object only if its entity tag (ETag) is the same as the one
* specified, otherwise return a 412 (precondition failed).
*/
copyOnlyIfMatch?: string;

/**
* Copy the object only if its entity tag (ETag) is different from the one
* specified, otherwise return a 304 (not modified).
*/
copyOnlyIfNoneMatch?: string;

/**
* Copy the object only if it has been modified since the specified time,
* otherwise return a 304 (not modified).
*/
copyOnlyIfModifiedSince?: Date;

/**
* Copy the object only if it has not been modified since the specified
* time, otherwise return a 412 (precondition failed).
*/
copyOnlyIfUnmodifiedSince?: Date;

// TOOD: better structured data
/** Gives the grantee READ, READ_ACP, and WRITE_ACP permissions on the object. */
grantFullControl?: string;

// TOOD: better structured data
/** Allows grantee to read the object data and its metadata. */
grantRead?: string;

// TOOD: better structured data
/** Allows grantee to write the ACL for the applicable object. */
grantReadAcp?: string;

// TOOD: better structured data
/** Allows grantee to write the ACL for the applicable object. */
grantWriteAcp?: string;

/**
* Specifies whether the metadata is copied from the source object or replaced
* with metadata provided in the request.
*/
metadataDirective?: CopyDirective;

/** Specifies whether a legal hold will be applied to this object. */
legalHold?: boolean;

/** The Object Lock mode that you want to apply to this object. */
lockMode?: LockMode;

/** The date and time when you want this object's Object Lock to expire. */
lockRetainUntil?: Date;

/**
* If you don't specify, S3 Standard is the default storage class.
* Amazon S3 supports other storage classes.
*/
storageClass?: StorageClass;

tags?: { [key: string]: string };

/**
* Specifies whether the object tag-set are copied from the source object or
* replaced with tag-set provided in the request.
*/
taggingDirective?: CopyDirective;

/**
* If the bucket is configured as a website, redirects requests for this
* object to another object in the same bucket or to an external URL.
* Amazon S3 stores the value of this header in the object metadata.
*/
websiteRedirectLocation?: string;
}

export interface CopyObjectResponse {
/**
* An ETag is an opaque identifier assigned by a web server to a
* specific version of a resource found at a URL.
*/
etag: string;

/**
* Version of the object.
*/
versionId?: string;
}

export interface DeleteObjectOptions {
versionId?: string;
}
Expand Down

0 comments on commit fa75e7a

Please sign in to comment.