Skip to content

Commit

Permalink
feat: head object api (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucacasonato committed Nov 21, 2020
1 parent a9d4fa5 commit 6a110d6
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 4 deletions.
91 changes: 91 additions & 0 deletions src/bucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
DeleteObjectResponse,
GetObjectOptions,
GetObjectResponse,
HeadObjectResponse,
ListAllObjectsOptions,
ListObjectsOptions,
ListObjectsResponse,
Expand Down Expand Up @@ -80,6 +81,96 @@ export class S3Bucket {
return fetch(signedRequest);
}

async headObject(
key: string,
options?: GetObjectOptions,
): Promise<HeadObjectResponse | undefined> {
const params: Params = {};
const headers: Params = {};
if (options?.ifMatch) headers["If-Match"] = options.ifMatch;
if (options?.ifNoneMatch) headers["If-None-Match"] = options.ifNoneMatch;
if (options?.ifModifiedSince) {
headers["If-Modified-Since"] = options.ifModifiedSince.toISOString();
}
if (options?.ifUnmodifiedSince) {
headers["If-Unmodified-Since"] = options.ifUnmodifiedSince.toISOString();
}
if (options?.partNumber) {
params["PartNumber"] = options.partNumber.toFixed(0);
}
if (options?.responseCacheControl) {
params["ResponseCacheControl"] = options.responseCacheControl;
}
if (options?.responseContentDisposition) {
params["ResponseContentDisposition"] = options.responseContentDisposition;
}
if (options?.responseContentEncoding) {
params["ResponseContentEncoding"] = options.responseContentEncoding;
}
if (options?.responseContentLanguage) {
params["ResponseContentLanguage"] = options.responseContentLanguage;
}
if (options?.responseContentType) {
params["ResponseContentType"] = options.responseContentType;
}
if (options?.responseExpires) {
params["ResponseExpires"] = options.responseExpires;
}
if (options?.versionId) {
params["VersionId"] = options.versionId;
}

const res = await this._doRequest(key, params, "HEAD", headers);
if (res.body) {
await res.arrayBuffer();
}
if (res.status === 404) {
return undefined;
}
if (res.status !== 200) {
throw new S3Error(
`Failed to get object: ${res.status} ${res.statusText}`,
await res.text(),
);
}

const expires = res.headers.get("expires");
const lockRetainUntil = res.headers.get(
"x-amz-object-lock-retain-until-date",
);
const partsCount = res.headers.get("x-amz-mp-parts-count");
const legalHold = res.headers.get("x-amz-object-lock-legal-hold");

return {
contentLength: parseInt(res.headers.get("Content-Length")!),
deleteMarker: res.headers.get("x-amz-delete-marker") === "true",
etag: JSON.parse(res.headers.get("etag")!),
lastModified: new Date(res.headers.get("Last-Modified")!),
missingMeta: parseInt(res.headers.get("x-amz-missing-meta") ?? "0"),
storageClass: res.headers.get("x-amz-storage-class") as StorageClass ??
"STANDARD",
taggingCount: parseInt(res.headers.get("x-amz-tagging-count") ?? "0"),

cacheControl: res.headers.get("Cache-Control") ?? undefined,
contentDisposition: res.headers.get("Content-Disposition") ?? undefined,
contentEncoding: res.headers.get("Content-Encoding") ?? undefined,
contentLanguage: res.headers.get("Content-Language") ?? undefined,
contentType: res.headers.get("Content-Type") ?? undefined,
expires: expires ? new Date(expires) : undefined,
legalHold: legalHold ? true : (legalHold === "OFF" ? false : undefined),
lockMode: res.headers.get("x-amz-object-lock-mode") as LockMode ??
undefined,
lockRetainUntil: lockRetainUntil ? new Date(lockRetainUntil) : undefined,
partsCount: partsCount ? parseInt(partsCount) : undefined,
replicationStatus:
res.headers.get("x-amz-replication-status") as ReplicationStatus ??
undefined,
versionId: res.headers.get("x-amz-version-id") ?? undefined,
websiteRedirectLocation:
res.headers.get("x-amz-website-redirect-location") ?? undefined,
};
}

async getObject(
key: string,
options?: GetObjectOptions,
Expand Down
31 changes: 31 additions & 0 deletions src/bucket_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,37 @@ Deno.test({
},
});

Deno.test({
name: "head object success",
async fn() {
// setup
await bucket.putObject(
"test",
encoder.encode("Test1"),
{ contentType: "text/plain" },
);

const head = await bucket.headObject("test");
assert(head);
assertEquals(head?.etag, "e1b849f9631ffc1829b2e31402373e3c");
assertEquals(head?.contentType, "text/plain");
assertEquals(head?.contentLength, 5);
assertEquals(head?.storageClass, "STANDARD");
assertEquals(head?.deleteMarker, false);
assert(new Date() >= (head?.lastModified ?? new Date(0)));

// teardown
await bucket.deleteObject("test");
},
});

Deno.test({
name: "head object not found",
async fn() {
assertEquals(await bucket.headObject("test2"), undefined);
},
});

Deno.test({
name: "get object success",
async fn() {
Expand Down
10 changes: 6 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,7 @@ export interface GetObjectOptions {
// TODO: range
}

export interface GetObjectResponse {
/** The body of this object. */
body: Uint8Array;

export interface HeadObjectResponse {
/** Specifies caching behavior along the request/reply chain. */
cacheControl?: string;

Expand Down Expand Up @@ -207,6 +204,11 @@ export interface GetObjectResponse {
// TODO: x-amz-restore
}

export interface GetObjectResponse extends HeadObjectResponse {
/** The body of this object. */
body: Uint8Array;
}

export interface ListObjectsResponse {
/**
* Set to false if all of the results were returned. Set to true if more keys are available to return. If the number of results exceeds that specified by MaxKeys, all of the results might not be returned.
Expand Down

0 comments on commit 6a110d6

Please sign in to comment.