update R2 storage to JSON format

This commit is contained in:
Jeff Emmett 2025-09-04 16:26:35 +02:00
parent 6cb70b4da3
commit 5fe28ba7f8
3 changed files with 187 additions and 5 deletions

View File

@ -23,8 +23,140 @@ export function CustomMainMenu() {
if (typeof event.target?.result !== 'string') {
return
}
const jsonData = JSON.parse(event.target.result) as TLContent
editor.putContentOntoCurrentPage(jsonData, { select: true })
try {
const jsonData = JSON.parse(event.target.result)
console.log('Parsed JSON data:', jsonData)
// Handle different JSON formats
let contentToImport: TLContent
// Check if it's a worker export format (has documents array)
if (jsonData.documents && Array.isArray(jsonData.documents)) {
console.log('Detected worker export format with', jsonData.documents.length, 'documents')
// Convert worker export format to TLContent format
const shapes = jsonData.documents
.filter((doc: any) => doc.state?.typeName === 'shape')
.map((doc: any) => doc.state)
const bindings = jsonData.documents
.filter((doc: any) => doc.state?.typeName === 'binding')
.map((doc: any) => doc.state)
const assets = jsonData.documents
.filter((doc: any) => doc.state?.typeName === 'asset')
.map((doc: any) => doc.state)
console.log('Extracted:', { shapes: shapes.length, bindings: bindings.length, assets: assets.length })
contentToImport = {
rootShapeIds: shapes.map((shape: any) => shape.id).filter(Boolean),
schema: jsonData.schema || { schemaVersion: 1, storeVersion: 4, recordVersions: {} },
shapes: shapes,
bindings: bindings,
assets: assets,
}
} else if (jsonData.shapes && Array.isArray(jsonData.shapes)) {
console.log('Detected standard TLContent format with', jsonData.shapes.length, 'shapes')
// Already in TLContent format, but ensure all required properties exist
contentToImport = {
rootShapeIds: jsonData.rootShapeIds || jsonData.shapes.map((shape: any) => shape.id).filter(Boolean),
schema: jsonData.schema || { schemaVersion: 1, storeVersion: 4, recordVersions: {} },
shapes: jsonData.shapes,
bindings: jsonData.bindings || [],
assets: jsonData.assets || [],
}
} else {
console.log('Detected unknown format, attempting fallback')
// Try to extract shapes from any other format
contentToImport = {
rootShapeIds: jsonData.rootShapeIds || [],
schema: jsonData.schema || { schemaVersion: 1, storeVersion: 4, recordVersions: {} },
shapes: jsonData.shapes || [],
bindings: jsonData.bindings || [],
assets: jsonData.assets || [],
}
}
// Validate all required properties
console.log('Final contentToImport:', contentToImport)
if (!contentToImport.shapes || !Array.isArray(contentToImport.shapes)) {
console.error('Invalid JSON format: missing or invalid shapes array')
alert('Invalid JSON format. Please ensure the file contains valid TLDraw content.')
return
}
if (!contentToImport.rootShapeIds || !Array.isArray(contentToImport.rootShapeIds)) {
console.error('Invalid JSON format: missing or invalid rootShapeIds array')
alert('Invalid JSON format. Please ensure the file contains valid TLDraw content.')
return
}
if (!contentToImport.schema) {
console.error('Invalid JSON format: missing schema')
alert('Invalid JSON format. Please ensure the file contains valid TLDraw content.')
return
}
if (!contentToImport.bindings || !Array.isArray(contentToImport.bindings)) {
contentToImport.bindings = []
}
if (!contentToImport.assets || !Array.isArray(contentToImport.assets)) {
contentToImport.assets = []
}
console.log('About to call putContentOntoCurrentPage with:', contentToImport)
try {
editor.putContentOntoCurrentPage(contentToImport, { select: true })
} catch (putContentError) {
console.error('putContentOntoCurrentPage failed, trying alternative approach:', putContentError)
// Fallback: Create shapes individually
if (contentToImport.shapes && contentToImport.shapes.length > 0) {
console.log('Attempting to create shapes individually...')
// Clear current page first
const currentShapes = editor.getCurrentPageShapes()
if (currentShapes.length > 0) {
editor.deleteShapes(currentShapes.map(shape => shape.id))
}
// Create shapes one by one
contentToImport.shapes.forEach((shape: any) => {
try {
if (shape && shape.id && shape.type) {
editor.createShape(shape)
}
} catch (shapeError) {
console.error('Failed to create shape:', shape, shapeError)
}
})
// Create bindings if any
if (contentToImport.bindings && contentToImport.bindings.length > 0) {
contentToImport.bindings.forEach((binding: any) => {
try {
if (binding && binding.id) {
editor.createBinding(binding)
}
} catch (bindingError) {
console.error('Failed to create binding:', binding, bindingError)
}
})
}
console.log('Individual shape creation completed')
} else {
alert('No valid shapes found in the JSON file.')
}
}
} catch (error) {
console.error('Error parsing JSON:', error)
alert('Error parsing JSON file. Please ensure the file is valid JSON.')
}
};
if (file) {
reader.readAsText(file);

View File

@ -270,7 +270,11 @@ export class TldrawDurableObject {
// convert the room to JSON and upload it to R2
const snapshot = JSON.stringify(room.getCurrentSnapshot())
await this.r2.put(`rooms/${this.roomId}`, snapshot)
await this.r2.put(`rooms/${this.roomId}`, snapshot, {
httpMetadata: {
contentType: 'application/json'
}
})
console.log(`Board persisted to R2: ${this.roomId}`)
}, 30_000)

View File

@ -595,8 +595,12 @@ async function backupAllBoards(env: Environment) {
// Create backup key with date only
const backupKey = `${date}/${room.key}`
// Store in backup bucket as JSON
await env.BOARD_BACKUPS_BUCKET.put(backupKey, jsonData)
// Store in backup bucket as JSON with proper content-type
await env.BOARD_BACKUPS_BUCKET.put(backupKey, jsonData, {
httpMetadata: {
contentType: 'application/json'
}
})
// Backed up successfully
} catch (error) {
@ -624,6 +628,48 @@ async function backupAllBoards(env: Environment) {
}
router
.get("/export/:roomId", async (request, env) => {
try {
const roomId = request.params.roomId
if (!roomId) {
return new Response(JSON.stringify({ error: 'Room ID is required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
})
}
// Get the room data from R2
const roomData = await env.TLDRAW_BUCKET.get(`rooms/${roomId}`)
if (!roomData) {
return new Response(JSON.stringify({ error: 'Room not found' }), {
status: 404,
headers: { 'Content-Type': 'application/json' }
})
}
// Get the JSON data
const jsonData = await roomData.text()
// Return as downloadable JSON file
return new Response(jsonData, {
headers: {
'Content-Type': 'application/json',
'Content-Disposition': `attachment; filename="${roomId}-board.json"`,
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': '*',
}
})
} catch (error) {
console.error('Export failed:', error)
return new Response(JSON.stringify({
error: 'Export failed',
message: (error as Error).message
}), {
status: 500,
headers: { 'Content-Type': 'application/json' }
})
}
})
.get("/backup", async (_, env) => {
const result = await backupAllBoards(env)
return new Response(JSON.stringify(result), {