remove s3

This commit is contained in:
Hymmel 2025-10-09 13:40:42 +02:00
parent afcaa0b217
commit df76a1f0f2
3 changed files with 1312 additions and 126 deletions

1286
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -11,7 +11,7 @@
},
"dependencies": {
"@prisma/client": "^5.17.0",
"@aws-sdk/client-s3": "^3.583.0",
"bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",

View file

@ -10,7 +10,7 @@ import { fileURLToPath } from 'url';
import { randomUUID } from 'crypto';
import dotenv from 'dotenv';
import { PrismaClient } from '@prisma/client';
import { S3Client, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';
dotenv.config();
@ -20,28 +20,7 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const uploadDir = path.join(__dirname, '..', 'uploads');
const fsPromises = fs.promises;
const assetBaseUrl = (process.env.ASSET_BASE_URL || '').replace(/\/$/, '');
const s3Bucket = process.env.S3_BUCKET || '';
const s3Region = process.env.S3_REGION || 'us-east-1';
const s3Endpoint = process.env.S3_ENDPOINT || undefined;
const s3AccessKey = process.env.S3_ACCESS_KEY || '';
const s3SecretKey = process.env.S3_SECRET_KEY || '';
const s3ForcePathStyle = parseBoolean(process.env.S3_FORCE_PATH_STYLE ?? 'true');
const useS3Storage = Boolean(s3Bucket);
const s3Client = useS3Storage
? new S3Client({
region: s3Region,
endpoint: s3Endpoint,
forcePathStyle: s3ForcePathStyle,
credentials:
s3AccessKey && s3SecretKey
? {
accessKeyId: s3AccessKey,
secretAccessKey: s3SecretKey,
}
: undefined,
})
: null;
const jwtSecret = process.env.JWT_SECRET || 'super-secret-key';
const defaultFrontendOrigins = ['http://localhost:3100', 'http://localhost:3000'];
const configuredOrigins = process.env.FRONTEND_ORIGINS || process.env.FRONTEND_ORIGIN || '';
@ -81,34 +60,26 @@ const corsOptions = {
const port = process.env.PORT || 4000;
const host = process.env.HOST || '0.0.0.0';
let upload;
if (useS3Storage) {
upload = multer({ storage: multer.memoryStorage() });
} else {
if (!fs.existsSync(uploadDir)) {
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
}
const diskStorage = multer.diskStorage({
const diskStorage = multer.diskStorage({
destination: (_req, _file, cb) => cb(null, uploadDir),
filename: (_req, file, cb) => {
const unique = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const ext = path.extname(file.originalname) || '.png';
cb(null, `${unique}${ext}`);
},
});
});
upload = multer({ storage: diskStorage });
}
const upload = multer({ storage: diskStorage });
app.use(cors(corsOptions));
app.options('*', cors(corsOptions));
app.use(express.json());
app.use(cookieParser());
if (!useS3Storage) {
app.use('/uploads', express.static(uploadDir));
}
app.use('/uploads', express.static(uploadDir));
function setAuthCookie(res, token) {
res.cookie('token', token, {
@ -319,13 +290,13 @@ app.post('/bases', requireAuth, upload.single('imageFile'), async (req, res) =>
try {
const { title, description, url, imageMode, imageUrl, isPrivate } = req.body;
if (!title || !title.trim()) {
if (!useS3Storage && req.file?.filename) {
if (req.file?.filename) {
await deleteImageFile(req.file.filename);
}
return res.status(400).json({ error: 'Title is required' });
}
if (url && !isValidUrl(url)) {
if (!useS3Storage && req.file?.filename) {
if (req.file?.filename) {
await deleteImageFile(req.file.filename);
}
return res.status(400).json({ error: 'Planning link must be a valid URL' });
@ -335,7 +306,7 @@ app.post('/bases', requireAuth, upload.single('imageFile'), async (req, res) =>
let storedImagePath = null;
if (imageMode === 'url') {
if (!imageUrl || !isValidUrl(imageUrl)) {
if (!useS3Storage && req.file?.filename) {
if (req.file?.filename) {
await deleteImageFile(req.file.filename);
}
return res.status(400).json({ error: 'Image URL must be valid' });
@ -374,7 +345,7 @@ app.post('/bases', requireAuth, upload.single('imageFile'), async (req, res) =>
console.error(error);
if (uploadedFile?.path) {
await deleteImageFile(uploadedFile.path);
} else if (!useS3Storage && req.file?.filename) {
} else if (req.file?.filename) {
await deleteImageFile(req.file.filename);
}
return res.status(500).json({ error: 'Internal server error' });
@ -396,14 +367,14 @@ app.put('/bases/:baseId', requireAuth, upload.single('imageFile'), async (req, r
} = req.body;
if (!title || !title.trim()) {
if (!useS3Storage && req.file?.filename) {
if (req.file?.filename) {
await deleteImageFile(req.file.filename);
}
return res.status(400).json({ error: 'Title is required' });
}
if (url && !isValidUrl(url)) {
if (!useS3Storage && req.file?.filename) {
if (req.file?.filename) {
await deleteImageFile(req.file.filename);
}
return res.status(400).json({ error: 'Planning link must be a valid URL' });
@ -411,7 +382,7 @@ app.put('/bases/:baseId', requireAuth, upload.single('imageFile'), async (req, r
const base = await prisma.base.findFirst({ where: { id: baseId, userId: req.user.id } });
if (!base) {
if (!useS3Storage && req.file?.filename) {
if (req.file?.filename) {
await deleteImageFile(req.file.filename);
}
return res.status(404).json({ error: 'Base not found' });
@ -431,7 +402,7 @@ app.put('/bases/:baseId', requireAuth, upload.single('imageFile'), async (req, r
} else if (imageMode === 'url') {
if (imageUrl) {
if (!isValidUrl(imageUrl)) {
if (!useS3Storage && req.file?.filename) {
if (req.file?.filename) {
await deleteImageFile(req.file.filename);
}
return res.status(400).json({ error: 'Image URL must be valid' });
@ -478,7 +449,7 @@ app.put('/bases/:baseId', requireAuth, upload.single('imageFile'), async (req, r
console.error(error);
if (uploadedFile?.path) {
await deleteImageFile(uploadedFile.path);
} else if (!useS3Storage && req.file?.filename) {
} else if (req.file?.filename) {
await deleteImageFile(req.file.filename);
}
return res.status(500).json({ error: 'Internal server error' });
@ -914,71 +885,16 @@ function buildStoredImageUrl(imagePath) {
return '';
}
if (useS3Storage) {
const base = assetBaseUrl || deriveS3EndpointBase();
if (!base) {
return '';
}
return `${base.replace(/\/$/, '')}/${imagePath.replace(/^\/+/, '')}`;
}
return `/uploads/${imagePath.replace(/^\/+/, '')}`;
}
function deriveS3EndpointBase() {
if (!s3Endpoint) {
return '';
}
const trimmedEndpoint = s3Endpoint.replace(/\/$/, '');
if (s3ForcePathStyle || !trimmedEndpoint) {
return `${trimmedEndpoint}/${s3Bucket}`;
}
try {
const endpointUrl = new URL(trimmedEndpoint);
const portPart = endpointUrl.port ? `:${endpointUrl.port}` : '';
return `${endpointUrl.protocol}//${s3Bucket}.${endpointUrl.hostname}${portPart}`;
} catch (_error) {
return `${trimmedEndpoint}/${s3Bucket}`;
}
}
function buildStorageKey(originalName = '') {
const ext = path.extname(originalName || '').trim().slice(0, 10) || '.png';
return `bases/${Date.now()}-${randomUUID()}${ext}`;
}
async function persistUploadedFile(file) {
if (!file) {
return null;
}
if (useS3Storage) {
const key = buildStorageKey(file.originalname);
try {
await s3Client.send(
new PutObjectCommand({
Bucket: s3Bucket,
Key: key,
Body: file.buffer,
ContentType: file.mimetype || 'application/octet-stream',
Metadata: {
originalname: file.originalname || 'uploaded-image',
},
})
);
} catch (error) {
console.error('Failed to upload image to S3', error);
throw error;
}
return {
path: key,
publicUrl: buildStoredImageUrl(key),
};
}
return {
path: file.filename,
publicUrl: buildStoredImageUrl(file.filename),
@ -988,22 +904,6 @@ async function persistUploadedFile(file) {
async function deleteImageFile(imagePath) {
if (!imagePath) return;
if (useS3Storage) {
try {
await s3Client.send(
new DeleteObjectCommand({
Bucket: s3Bucket,
Key: imagePath,
})
);
} catch (error) {
if (error?.$metadata?.httpStatusCode !== 404) {
console.error(`Failed to delete image from S3 with key ${imagePath}`, error);
}
}
return;
}
const filePath = path.isAbsolute(imagePath) ? imagePath : path.join(uploadDir, imagePath);
try {
await fsPromises.unlink(filePath);