diff --git a/src/routes/files.ts b/src/routes/files.ts index 37a4e496d6..28087053c9 100644 --- a/src/routes/files.ts +++ b/src/routes/files.ts @@ -1,11 +1,61 @@ import express from 'express'; import asyncHandler from 'express-async-handler'; +import Busboy from 'busboy'; import sanitizeQuery from '../middleware/sanitize-query'; import validateQuery from '../middleware/validate-query'; import * as FilesService from '../services/files'; +import storage from '../storage'; +import { Readable } from 'stream'; const router = express.Router(); +router.post( + '/', + asyncHandler(async (req, res, next) => { + const busboy = new Busboy({ headers: req.headers }); + + /** + * The order of the fields in multipart/form-data is important. We require that storage is set + * before the file contents itself. This allows us to set and use the correct storage adapter + * for the file upload. + */ + + let disk: string; + + busboy.on('field', (fieldname, val) => { + if (fieldname === 'storage') { + disk = val; + } + }); + + busboy.on('file', async (fieldname, fileStream, filename, encoding, mimetype) => { + if (!disk) { + return busboy.emit('error', new Error('no storage provided')); + } + + try { + await storage.disk(disk).put(filename, fileStream as Readable); + } catch (err) { + busboy.emit('error', err); + } + + fileStream.on('end', () => { + console.log(`File ${filename} saved.`); + }); + }); + + busboy.on('error', (error: Error) => { + next(error); + }); + + busboy.on('finish', () => { + res.status(200).end(); + }); + + return req.pipe(busboy); + }) +); + /** @TODO This needs to support multipart form-data for file uploads */ // router.post( // '/', diff --git a/src/storage.ts b/src/storage.ts new file mode 100644 index 0000000000..88c72f214f --- /dev/null +++ b/src/storage.ts @@ -0,0 +1,68 @@ +import { + StorageManager, + LocalFileSystemStorage, + StorageManagerConfig, + Storage, +} from '@slynova/flydrive'; +import camelcase from 'camelcase'; + +import { AmazonWebServicesS3Storage } from '@slynova/flydrive-s3'; +import { GoogleCloudStorage } from '@slynova/flydrive-gcs'; + +/** @todo dynamically load storage adapters here */ + +console.log(getStorageConfig()); +const storage = new StorageManager(getStorageConfig()); + +registerDrivers(storage); + +export default storage; + +function getStorageConfig(): StorageManagerConfig { + const config: any = { disks: {} }; + + for (const [key, value] of Object.entries(process.env)) { + if (key.startsWith('STORAGE') === false) continue; + if (key === 'STORAGE_LOCATIONS') continue; + if (key.endsWith('PUBLIC_URL')) continue; + + const disk = key.split('_')[1].toLowerCase(); + if (!config.disks[disk]) config.disks[disk] = { config: {} }; + + if (key.endsWith('DRIVER')) { + config.disks[disk].driver = value; + continue; + } + + const configKey = camelcase( + key.split('_').filter((_, index) => [0, 1].includes(index) === false) + ); + config.disks[disk].config[configKey] = value; + } + + return config; +} + +function registerDrivers(storage: StorageManager) { + const usedDrivers: string[] = []; + + for (const [key, value] of Object.entries(process.env)) { + if ((key.startsWith('STORAGE') && key.endsWith('DRIVER')) === false) continue; + if (usedDrivers.includes(value) === false) usedDrivers.push(value); + } + + usedDrivers.forEach((driver) => { + storage.registerDriver(driver, getStorageDriver(driver)); + }); +} + +function getStorageDriver(driver: string) { + switch (driver) { + case 'local': + return LocalFileSystemStorage; + case 's3': + return AmazonWebServicesS3Storage; + case 'gcs': + return GoogleCloudStorage; + } +}