Ctrl K
ComunidadGuías…

scripts/admin_image_storage.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { S3Client } from '@bradenmacdonald/s3-lite-client'

// Admin-script access to avatars, org logos and app icons. They live in R2
// under the `images/` prefix of the main bucket (see
// supabase/functions/_backend/utils/image_storage.ts); Supabase Storage is no
// longer used for images.
export const IMAGE_KEY_PREFIX = 'images/'

export interface ImageStore {
  bucket: string
  exists: (path: string) => Promise<boolean>
  lastModified: (path: string) => Promise<Date | null>
  /** Last-modified time of every image object under a folder ('' = all), keyed by image path. */
  listLastModified: (prefix: string) => Promise<Map<string, Date>>
  download: (path: string) => Promise<Blob | null>
  upload: (path: string, data: Blob | Uint8Array | ArrayBuffer, options?: { contentType?: string, upsert?: boolean }) => Promise<'ok' | 'conflict'>
  copy: (fromPath: string, toPath: string) => Promise<void>
  remove: (paths: string[]) => Promise<void>
}

function requireEnv(env: Record<string, string | undefined>, key: string) {
  const value = env[key]?.trim()
  if (!value)
    throw new Error(`Missing ${key}`)
  return value
}

function isNotFound(error: unknown) {
  if (!error || typeof error !== 'object')
    return false
  const candidate = error as { code?: unknown, status?: unknown, statusCode?: unknown }
  return candidate.statusCode === 404 || candidate.status === 404 || candidate.code === 'NoSuchKey'
}

export function imageObjectKey(path: string) {
  return `${IMAGE_KEY_PREFIX}${path.replace(/^\/+/, '')}`
}

export function createImageStore(env: Record<string, string | undefined>): ImageStore {
  const rawEndpoint = requireEnv(env, 'S3_ENDPOINT')
  const protocol = env.S3_SSL === 'false' ? 'http' : 'https'
  const bucket = requireEnv(env, 'S3_BUCKET')
  const client = new S3Client({
    endPoint: rawEndpoint.includes('://') ? rawEndpoint : `${protocol}://${rawEndpoint}`,
    accessKey: requireEnv(env, 'S3_ACCESS_KEY_ID'),
    secretKey: requireEnv(env, 'S3_SECRET_ACCESS_KEY'),
    region: env.S3_REGION?.trim() || 'us-east-1',
    pathStyle: true,
    bucket,
  })

  async function upload(path: string, data: Blob | Uint8Array | ArrayBuffer, options: { contentType?: string, upsert?: boolean } = {}) {
    const key = imageObjectKey(path)
    if (!options.upsert && await client.exists(key))
      return 'conflict' as const
    let bytes: Uint8Array
    if (data instanceof Uint8Array)
      bytes = data
    else if (data instanceof ArrayBuffer)
      bytes = new Uint8Array(data)
    else
      bytes = new Uint8Array(await data.arrayBuffer())
    const contentType = options.contentType || (data instanceof Blob ? data.type : '') || 'application/octet-stream'
    await client.putObject(key, bytes, {
      size: bytes.byteLength,
      metadata: { 'Content-Type': contentType, 'Cache-Control': 'private, max-age=0' },
    })
    return 'ok' as const
  }

  return {
    bucket,
    exists: path => client.exists(imageObjectKey(path)),
    async lastModified(path) {
      try {
        return (await client.statObject(imageObjectKey(path))).lastModified
      }
      catch (error) {
        if (isNotFound(error))
          return null
        throw error
      }
    },
    async listLastModified(prefix) {
      const trimmed = prefix.replace(/^\/+|\/+$/g, '')
      const listPrefix = trimmed ? `${IMAGE_KEY_PREFIX}${trimmed}/` : IMAGE_KEY_PREFIX
      const index = new Map<string, Date>()
      for await (const object of client.listObjects({ prefix: listPrefix }))
        index.set(object.key.slice(IMAGE_KEY_PREFIX.length), object.lastModified)
      return index
    },
    async download(path) {
      try {
        const response = await client.getObject(imageObjectKey(path))
        return await response.blob()
      }
      catch (error) {
        if (isNotFound(error))
          return null
        throw error
      }
    },
    upload,
    async copy(fromPath, toPath) {
      await client.copyObject({ sourceKey: imageObjectKey(fromPath) }, imageObjectKey(toPath))
    },
    async remove(paths) {
      for (const path of paths) {
        try {
          await client.deleteObject(imageObjectKey(path))
        }
        catch (error) {
          if (!isNotFound(error))
            throw error
        }
      }
    },
  }
}