remove s3
This commit is contained in:
parent
afcaa0b217
commit
df76a1f0f2
3 changed files with 1312 additions and 126 deletions
1286
backend/package-lock.json
generated
Normal file
1286
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
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 });
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
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}`);
|
||||
},
|
||||
});
|
||||
|
||||
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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue