All files / src/core/services R2Service.ts

20% Statements 3/15
0% Branches 0/1
0% Functions 0/9
21.42% Lines 3/14

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 13628x                             28x                     28x                                                                                                                                                                                                                          
import { R2Binding } from '@/core/bindings/R2Binding'
 
import type { ObjectStore, StoredObject } from '@bayudwiyansatria/core'
import type { CloudflareEnv } from '@/types/CloudflareEnv'
 
export type { StoredObject } from '@bayudwiyansatria/core'
 
/**
 * R2 bucket binding.
 *
 * Every setting it needs — the bucket's binding name — arrives resolved from
 * the configuration layer, so nothing about it is decided here. The binding
 * itself is read from `env` per call, which is what makes one module-scope
 * instance safe to share.
 */
const storage = new R2Binding()
 
/**
 * Object-storage capability, backed by R2.
 *
 * @class
 *
 * @author Bayu Dwiyan Satria
 * @version 1.0.0
 * @since 1.0.0
 */
export class R2Service<E extends CloudflareEnv = CloudflareEnv> implements ObjectStore<E> {
  /**
   * Stores an object.
   *
   * @param env The Worker environment.
   * @param key The object key.
   * @param body The object body.
   * @param contentType MIME type recorded on the object.
   * @returns The stored object's key and size.
   */
  public async upload(
    env: E,
    key: string,
    body: ReadableStream | ArrayBuffer | ArrayBufferView | string | Blob,
    contentType = 'application/octet-stream'
  ): Promise<StoredObject> {
    const object = await storage.put(env, key, body, { httpMetadata: { contentType } })
 
    return { key: object.key, size: object.size }
  }
 
  /**
   * Stores a value as JSON.
   *
   * @param env The Worker environment.
   * @param key The object key.
   * @param value The value to serialise.
   * @returns The stored object's key and size.
   */
  public async uploadJson(env: E, key: string, value: unknown): Promise<StoredObject> {
    const object = await storage.putJson(env, key, value)
 
    return { key: object.key, size: object.size }
  }
 
  /**
   * Reads an object as text.
   *
   * @param env The Worker environment.
   * @param key The object key.
   * @returns The body, or `null` when the object does not exist.
   */
  public async download(env: E, key: string): Promise<string | null> {
    return await storage.getText(env, key)
  }
 
  /**
   * Reads an object and parses it as JSON.
   *
   * @param env The Worker environment.
   * @param key The object key.
   * @returns The parsed body, or `null` when the object does not exist.
   */
  public async downloadJson<T = unknown>(env: E, key: string): Promise<T | null> {
    return await storage.getJson<T>(env, key)
  }
 
  /**
   * Checks whether an object exists.
   *
   * Reads metadata only, so no body is transferred.
   *
   * @param env The Worker environment.
   * @param key The object key.
   * @returns `true` when the object exists.
   */
  public async exists(env: E, key: string): Promise<boolean> {
    return (await storage.head(env, key)) !== null
  }
 
  /**
   * Deletes one or more objects.
   *
   * Deleting a key that does not exist is not an error.
   *
   * @param env The Worker environment.
   * @param keys A key, or a list of keys.
   */
  public async remove(env: E, keys: string | string[]): Promise<void> {
    await storage.delete(env, keys)
  }
 
  /**
   * Lists the objects under a prefix.
   *
   * @remarks
   * One page only — R2 paginates, and a full walk is rarely what a request
   * handler wants.
   *
   * @param env The Worker environment.
   * @param prefix The key prefix to match.
   * @returns The matched objects.
   */
  public async list(env: E, prefix: string): Promise<StoredObject[]> {
    const page = await storage.list(env, { prefix })
 
    return page.objects.map(object => ({ key: object.key, size: object.size }))
  }
 
  /**
   * Reports whether the bucket is bound to this Worker.
   *
   * @param env The Worker environment.
   * @returns `true` when the binding is present.
   */
  public isAvailable(env: E): boolean {
    return storage.isBound(env)
  }
}