Volver a la wiki

notebook_attachments — Tabla D1 y endpoints de imágenes embebidas en notas

notebook_attachments — Tabla D1 y endpoints de imágenes embebidas

Entidad introducida en la migración 0032_notebook_sharing.sql (commit ab1c02f, 2026-05-20). Gestiona las imágenes embebidas en las notas del Cuaderno, usando el R2 bucket TASK_ATTACHMENTS con prefijo de clave notebook/.

Esquema D1

CREATE TABLE IF NOT EXISTS notebook_attachments (
  id             INTEGER PRIMARY KEY AUTOINCREMENT,
  note_id        INTEGER NOT NULL,
  r2_key         TEXT    NOT NULL,
  filename       TEXT    NOT NULL,
  content_type   TEXT    NOT NULL,
  size_bytes     INTEGER NOT NULL,
  uploaded_by    TEXT    NOT NULL,
  uploaded_at    TEXT    NOT NULL DEFAULT (datetime('now')),
  FOREIGN KEY (note_id) REFERENCES notebook_notes(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_notebook_attachments_note ON notebook_attachments(note_id);

Tipos de imagen permitidos

MIME typeExtensión
image/png.png
image/jpeg.jpg
image/webp.webp
image/gif.gif

Límite de tamaño: 5 MB por imagen (MAX_SIZE_BYTES = 5 * 1024 * 1024).

Clave R2

Patrón: notebook/{note_id}/{uuid}.{ext}

Ejemplo: notebook/42/c3d9a1b2-…-f7e8.webp

El bucket es TASK_ATTACHMENTS (shared con el módulo de tareas, que usa prefijo tasks/).

Endpoint: POST /api/notebook/[id]/images

Archivo: functions/api/notebook/[id]/images.ts

Acceso

Owner OR collaborador (cualquier email en shared_with).

Request

multipart/form-data con campo file (File).

Flujo

  1. Verifica acceso a la nota (hasAccess).
  2. Valida tamaño y content-type.
  3. Genera r2_key = notebook/{noteId}/{uuid}.{ext}.
  4. TASK_ATTACHMENTS.put(r2Key, buffer, { httpMetadata, customMetadata }).
  5. INSERT INTO notebook_attachments … RETURNING id.
  6. Registra actividad (entity_type: 'notebook_attachment', action: 'create').
  7. Devuelve { id, url: '/api/notebook-images/{id}', filename, content_type } — 201 Created.

Errores

CódigoMotivo
400note_id inválido, form-data malformada, campo file ausente, fichero vacío
403Sin acceso a la nota
404Nota no encontrada
413Fichero > 5 MB
415Content-type no soportado
500Fallo al registrar en D1

Endpoint: GET /api/notebook-images/[id]

Archivo: functions/api/notebook-images/[id].ts

Acceso

Idéntico al de la nota padre: owner OR shared_with = 'ALL' OR email en shared_with.

Flujo

  1. Busca fila en notebook_attachments por id.
  2. Busca nota padre para verificar acceso.
  3. TASK_ATTACHMENTS.get(r2_key).
  4. Stream del object.body con headers adecuados.

Headers de respuesta

Content-Type: <content_type del adjunto>
Content-Length: <size_bytes>
Content-Disposition: inline; filename="<encoded_filename>"
Cache-Control: private, max-age=3600

Errores

CódigoMotivo
400id no es entero
403Sin acceso a la nota padre
404Adjunto o nota no encontrados
410El adjunto existe en D1 pero el objeto R2 ya no existe (object gone)

Uso desde el editor

El botón 🖼 del toolbar de NoteEditor hace POST al endpoint, recibe la URL y la inserta como Markdown:

![descripción](/api/notebook-images/42)

El renderizador Markdown del editor muestra la imagen embebida en la preview.

Orphaned R2 objects

Al borrar una nota, el handler DELETE /api/notebook/[id] debe:

  1. Consultar todos los r2_key de notebook_attachments WHERE note_id = ?.
  2. Llamar a TASK_ATTACHMENTS.delete(r2_key) para cada uno.
  3. Después ejecutar el DELETE FROM notebook_notes (que limpia D1 por CASCADE).

Si el paso 2 falla a medias, quedan objetos huérfanos en R2. No existe job de reconciliación actualmente.

Véase también

Subir