- Fix transform box not updating after alignment/arrangement/normalize/flip operations (shortcuts, context menu, and selection toolbar all fixed with DRY _opUpdate helper) - Replace 100ms polling loop with event-driven viewport updates (moved + wheel-scroll) - Add adaptive dot grid background that responds to zoom/pan - Reduce snap guide threshold from 8px to 4px for subtler snapping - Remove PresenceOverlay (remote selection highlighting) — too heavy for minimal benefit - Offset multiple dropped images so they don't overlap - Add new canvas modules: SnapGuides, clipboard, grouping, FrameSprite, DrawingSprite, LaserPointer, context-menu-items, SelectionToolbar, Minimap - Extract Editor hooks into dedicated files (useBoardLoader, useCanvasSetup, useShortcutHandler, useLayerPanel, useSaveManager, useFollowMode) - Sync improvements: real-time transform broadcast, board rooms, viewport sync
133 lines
3.5 KiB
JavaScript
133 lines
3.5 KiB
JavaScript
const Minio = require('minio');
|
|
const path = require('path');
|
|
|
|
const MINIO_ENDPOINT = process.env.MINIO_ENDPOINT || 'localhost';
|
|
const MINIO_PORT = parseInt(process.env.MINIO_PORT || '9000', 10);
|
|
const MINIO_ACCESS_KEY = process.env.MINIO_ACCESS_KEY || 'minioadmin';
|
|
const MINIO_SECRET_KEY = process.env.MINIO_SECRET_KEY || 'minioadmin';
|
|
const MINIO_USE_SSL = process.env.MINIO_USE_SSL === 'true';
|
|
const MINIO_BUCKET = process.env.MINIO_BUCKET || 'refboard';
|
|
const PUBLIC_URL = process.env.PUBLIC_URL || `http://${MINIO_ENDPOINT}:${MINIO_PORT}`;
|
|
|
|
const minioClient = new Minio.Client({
|
|
endPoint: MINIO_ENDPOINT,
|
|
port: MINIO_PORT,
|
|
useSSL: MINIO_USE_SSL,
|
|
accessKey: MINIO_ACCESS_KEY,
|
|
secretKey: MINIO_SECRET_KEY,
|
|
});
|
|
|
|
const MIME_TO_EXT = {
|
|
'image/png': '.png',
|
|
'image/jpeg': '.jpg',
|
|
'image/gif': '.gif',
|
|
'image/webp': '.webp',
|
|
'image/svg+xml': '.svg',
|
|
'video/mp4': '.mp4',
|
|
'video/webm': '.webm',
|
|
'video/quicktime': '.mov',
|
|
};
|
|
|
|
/**
|
|
* Create the bucket if it doesn't already exist.
|
|
* Sets a public read policy so images are accessible via URL.
|
|
*/
|
|
async function initBucket() {
|
|
const exists = await minioClient.bucketExists(MINIO_BUCKET);
|
|
if (!exists) {
|
|
await minioClient.makeBucket(MINIO_BUCKET, '');
|
|
// Set public read policy
|
|
const policy = {
|
|
Version: '2012-10-17',
|
|
Statement: [
|
|
{
|
|
Effect: 'Allow',
|
|
Principal: { AWS: ['*'] },
|
|
Action: ['s3:GetObject'],
|
|
Resource: [`arn:aws:s3:::${MINIO_BUCKET}/*`],
|
|
},
|
|
],
|
|
};
|
|
await minioClient.setBucketPolicy(MINIO_BUCKET, JSON.stringify(policy));
|
|
}
|
|
console.log(`[minio] Bucket "${MINIO_BUCKET}" ready`);
|
|
}
|
|
|
|
/**
|
|
* Upload an image buffer to MinIO.
|
|
* Returns the minio object path (used as key for future operations).
|
|
*/
|
|
async function uploadImage(boardId, imageId, buffer, mimeType) {
|
|
const ext = MIME_TO_EXT[mimeType] || '.bin';
|
|
const objectName = `boards/${boardId}/${imageId}${ext}`;
|
|
|
|
await minioClient.putObject(MINIO_BUCKET, objectName, buffer, buffer.length, {
|
|
'Content-Type': mimeType,
|
|
});
|
|
|
|
return objectName;
|
|
}
|
|
|
|
/**
|
|
* Upload a raw buffer to MinIO at the given object name.
|
|
* Returns the object name.
|
|
*/
|
|
async function putBuffer(objectName, buffer, contentType) {
|
|
await minioClient.putObject(MINIO_BUCKET, objectName, buffer, buffer.length, {
|
|
'Content-Type': contentType,
|
|
});
|
|
return objectName;
|
|
}
|
|
|
|
/**
|
|
* Delete a single object by its minio path.
|
|
*/
|
|
async function deleteImage(minioPath) {
|
|
await minioClient.removeObject(MINIO_BUCKET, minioPath);
|
|
}
|
|
|
|
/**
|
|
* Delete all objects under the boards/{boardId}/ prefix.
|
|
*/
|
|
async function deleteBoardImages(boardId) {
|
|
const prefix = `boards/${boardId}/`;
|
|
const objectsList = [];
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const stream = minioClient.listObjectsV2(MINIO_BUCKET, prefix, true);
|
|
stream.on('data', (obj) => {
|
|
objectsList.push(obj.name);
|
|
});
|
|
stream.on('error', reject);
|
|
stream.on('end', async () => {
|
|
if (objectsList.length > 0) {
|
|
await minioClient.removeObjects(MINIO_BUCKET, objectsList);
|
|
}
|
|
resolve(objectsList.length);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Build the public URL for an image given its minio path.
|
|
*/
|
|
function getImageUrl(minioPath) {
|
|
// Return relative URL — images served via backend proxy at /api/images/*
|
|
return `/api/images/${minioPath}`;
|
|
}
|
|
|
|
const MAX_FILE_SIZE = parseInt(process.env.MAX_FILE_SIZE_MB || '200', 10) * 1024 * 1024;
|
|
|
|
module.exports = {
|
|
minioClient,
|
|
initBucket,
|
|
uploadImage,
|
|
putBuffer,
|
|
deleteImage,
|
|
deleteBoardImages,
|
|
getImageUrl,
|
|
MINIO_BUCKET,
|
|
MIME_TO_EXT,
|
|
MAX_FILE_SIZE,
|
|
};
|