Browse Source

Finish refactor - create upload middleware, stores, config, deployment

master
garrettmills 2 years ago
parent
commit
e8e4c0d23e
  1. 1
      .gitignore
  2. 125
      UploadUnit.js
  3. 12
      deploy/app/routing/middleware/upload/UploadFile.middleware.js
  4. 33
      deploy/config/upload.config.js
  5. 17
      index.js
  6. 59
      middleware/UploadFile.js
  7. 46
      model/File.js
  8. 2
      package.json
  9. 82
      store/FlitterStore.js
  10. 58
      store/Store.js
  11. 23
      yarn.lock

1
.gitignore

@ -0,0 +1 @@
node_modules*

125
UploadUnit.js

@ -1,26 +1,79 @@
/**
* @module flitter-upload/UploadUnit
*/
const Unit = require('libflitter/Unit')
const File = require('./model/File')
const FlitterStore = require('./store/FlitterStore')
const ncp = require('ncp')
const path = require('path')
/**
* Unit that prepares and provides services for uploading,
* accessing, and sending files.
* @extends module:libflitter/Unit~Unit
*/
class UploadUnit extends Unit {
/**
* Defines the services required by this unit.
* @returns {Array<string>}
*/
static get services() {
return [...super.services, 'models', 'configs', 'output', 'canon']
return [...super.services, 'models', 'configs', 'output', 'canon', 'utility']
}
/**
* Get the name of the service provided by this unit: 'upload'
* @returns {string} - 'upload'
*/
static get name() {
return 'upload'
}
/**
* Object mapping store type names to static class references
* for the store types supported by flitter-upload.
*
* Should map string -> {@link module:flitter-upload/store/Store~Store} class definitions.
*
* Supported types:
* - FlitterStore
*
* @type {object}
*/
store_classes = { FlitterStore }
/**
* Store configurations loaded from 'configs::upload.stores'.
* @type {object}
*/
store_configs = {}
/**
* Name of the default store. False if not set. Loaded
* from 'configs::upload.default_store'.
* @type {boolean|string}
*/
default_store_name = false
/**
* Mapping of store names to instances of the stores themselves.
* e.g. maps string -> {@link module:flitter-upload/store/Store~Store}.
* @type {object}
*/
stores = {}
/**
* Initialize the unit. Load the configurations from config files,
* then instantiate the configured stores.
* @param {module:libflitter/app/FlitterApp~FlitterApp} app - the application
* @returns {Promise<void>}
*/
async go(app) {
const di = app.di()
this.store_configs = this.configs.get('upload.stores')
this.default_store_name = this.configs.get('upload.default_store')
const upload_config = this.configs.get('upload')
this.store_configs = upload_config ? upload_config.stores : undefined
this.default_store_name = upload_config ? upload_config.default_store : undefined
this.models.external_model(this, 'File', File)
@ -28,23 +81,65 @@ class UploadUnit extends Unit {
di.make(provider)
}
for ( const store_name in this.store_configs ) {
if ( !this.store_configs.hasOwnProperty(store_name) ) continue
const config = this.store_configs[store_name]
config.name = store_name
if ( !config.type || !(Object.keys(this.store_classes).includes(config.type)) ) {
this.output.warn(`Invalid or missing upload store type for ${store_name}.`)
continue
}
if ( this.store_configs ) {
for (const store_name in this.store_configs) {
if (!this.store_configs.hasOwnProperty(store_name)) continue
const config = this.store_configs[store_name]
config.name = store_name
if (!config.type || !(Object.keys(this.store_classes).includes(config.type))) {
this.output.warn(`Invalid or missing upload store type for ${store_name}.`)
continue
}
const StoreClass = this.store_classes[config.type]
const store = new StoreClass(config)
this.stores[store_name] = store
const StoreClass = this.store_classes[config.type]
const store = new StoreClass(config)
await store.init()
this.stores[store_name] = store
}
}
this.canon.register_resource('uploader', (name) => this.get(name))
}
/**
* Handler for the 'upload' deployment. This creates the 'upload:UploadFile'
* middleware, and the upload configuration.
* @returns {Promise<void>}
*/
async deploy() {
const package_dir = __dirname
const base_dir = path.dirname(this.utility.root())
this.output.info(`Deploying uploader resources from ${package_dir} to ${base_dir}.`, 0)
function do_copy(from, to){
return new Promise(
(resolve, reject) => {
ncp(from, to, (error) => {
if ( error ) reject(error)
resolve()
})
}
)
}
await do_copy(path.resolve(package_dir, 'deploy'), base_dir)
}
/**
* Get the default file store provider.
* @returns {module:flitter-upload/store/Store~Store}
*/
provider() {
return this.get(this.default_store_name)
}
/**
* Get a configured store by name.
* This is registered as the 'uploader' canonical resolver.
* @param {string} store_name
* @returns {module:flitter-upload/store/Store~Store}
*/
get(store_name) {
return this.stores[store_name]
}

12
deploy/app/routing/middleware/upload/UploadFile.middleware.js

@ -0,0 +1,12 @@
const Middleware = require('flitter-upload/middleware/UploadFile')
/*
* Middleware to upload the files included in the request
* to the default file store backend. Stores instances of
* the "upload::File" model in "request.uploads".
*/
class UploadFile extends Middleware {
}
module.exports = exports = UploadFile

33
deploy/config/upload.config.js

@ -0,0 +1,33 @@
/*
* flitter-upload configuration
* ---------------------------------------------------------------
* Specifies the configuration for various uploader aspects. Mainly,
* contains the configuration for the different file upload backends.
*/
const upload_config = {
/*
* The name of the upload backend to use by default.
*/
default_store: 'flitter',
/*
* Stores available to the uploader.
*/
stores: {
/*
* Example of the basic, filesystem-backed uploader.
* The name of the store is arbitrary. Here, it's called 'flitter'.
*/
flitter: {
// This is a filesystem backed 'FlitterStore'
type: 'FlitterStore',
// Destination for uploaded files. Will be relative to the root
// path of the application.
destination: './uploads',
},
},
}
module.exports = exports = upload_config

17
index.js

@ -0,0 +1,17 @@
/**
* @module flitter-upload
*/
const UploadUnit = require('./UploadUnit')
const File = require('./model/File')
const FlitterStore = require('./store/FlitterStore')
const Store = require('./store/Store')
const UploadFile = require('./middleware/UploadFile')
module.exports = exports = {
UploadUnit,
File,
FlitterStore,
Store,
UploadFile,
}

59
middleware/UploadFile.js

@ -0,0 +1,59 @@
/**
* @module flitter-upload/middleware/UploadFile
*/
const Middleware = require('libflitter/middleware/Middleware')
/**
* Middleware that uploads the files provided in the request
* to the default backend store and places references to the
* instantiated {@link module:flitter-upload/model/File~File}
* records in "request.uploads".
* @extends module:libflitter/middleware/Middleware~Middleware
*/
class UploadFile extends Middleware {
/**
* Defines the services required by this middleware.
* @returns {Array<string>}
*/
static get services() {
return [...super.services, 'upload', 'models']
}
/**
* Executes the middleware. For any files that exist in the request,
* they will be stored in the {@link module:flitter-upload/store/Store~Store}
* that is configured as the default.
*
* This creates instances of {@link module:flitter-upload/model/File~File} and
* places them in req.uploads in an object mapping the upload field name to the
* File model instance.
*
* @param {express/request} req - the request
* @param {express/response} res - the response
* @param {function} next - the next function in the chain
* @param {object} [args = {}] - optional arguments
* @returns {Promise<void>}
*/
async test(req, res, next, args = {}){
const uploader = this.upload.provider()
const uploads = {}
if ( req.files ) {
for ( const field_name in req.files ) {
if ( !req.files.hasOwnProperty(field_name) ) continue
const file = req.files[field_name]
uploads[field_name] = await uploader.store({
temp_path: file.file,
original_name: file.filename,
mime_type: file.mimetype,
})
}
}
req.uploads = uploads
next()
}
}
module.exports = exports = UploadFile

46
model/File.js

@ -1,6 +1,35 @@
/**
* @module flitter-upload/model/File
*/
const { Model } = require('flitter-orm')
/**
* Represents a file stored in some backend and
* contains information about that file, and helper
* methods that can be used to access it.
* @extends module:flitter-orm/src/model/Model~Model
*/
class File extends Model {
/**
* Defines the services required by this model.
* @returns {Array<string>}
*/
static get services() {
return [...super.services, 'upload']
}
/**
* Get the schema definition for this model. Provides the following fields:
* - original_name (String) - the original name of the file
* - upload_name (String) - the name of the file as it exists in the store
* - mime_type (String)
* - upload_date (Date = now)
* - store (String) - the name of the store this file exists in
* - store_id (String) - some unique identifier for this file used by the store
* - discarded (Boolean = false) - if true, the file has been "discarded" by the user
* @returns {{store_id: StringConstructor, discarded: {default: boolean, type: BooleanConstructor}, mime_type: StringConstructor, original_name: StringConstructor, store: StringConstructor, upload_name: StringConstructor, upload_date: {default: (function(): Date), type: DateConstructor}}}
*/
static get schema() {
return {
original_name: String,
@ -12,6 +41,23 @@ class File extends Model {
discarded: { type: Boolean, default: false },
}
}
/**
* Get the upload store associated with this file.
* @returns {module:flitter-upload/store/Store~Store}
*/
provider() {
return this.upload.get(this.store)
}
/**
* Send the file represented by this model as the response.
* @param {express/response} response
* @returns {Promise<void>}
*/
async send(response) {
await this.provider().send_file(this, response)
}
}
module.exports = exports = File

2
package.json

@ -8,6 +8,8 @@
"license": "MIT",
"dependencies": {
"es6-promisify": "^6.0.1",
"mkdirp": "^1.0.3",
"ncp": "^2.0.0",
"uuid": "^3.3.2"
}
}

82
store/FlitterStore.js

@ -1,7 +1,89 @@
/**
* @module flitter-upload/store/FlitterStore
*/
const Store = require('./Store')
const path = require('path')
const mkdirp = require('mkdirp')
const fs = require('fs').promises
const uuid = require('uuid/v4')
/**
* Local filesystem-backed file store provider.
* @extends module:flitter-upload/store/Store~Store
*/
class FlitterStore extends Store {
/**
* The absolute path to the directory where uploads will be stored.
* @type {string}
*/
store_path = ''
/**
* Defines the services required by this store.
* @returns {Array<string>}
*/
static get services() {
return [...super.services, 'utility', 'output', 'models']
}
/**
* Initializes the store. Creates the store upload directory, if it does not already exist.
* @returns {Promise<void>}
*/
async init() {
this.store_path = path.resolve(path.dirname(this.utility.root()), this.config.destination ? this.config.destination : './uploads')
await mkdirp(this.store_path)
this.output.info(`Created ${this.config.name} upload destination: ${this.store_path}`)
}
/**
* Permanently store a temporarily uploaded file in this store.
* @param {object} params
* @param {string} params.temp_path - absolute path to the temporarily uploaded file
* @param {string} params.original_name - the original upload name of the file
* @param {string} params.mime_type - the MIME type of the file.
* @returns {Promise<module:flitter-upload/model/File~File>}
*/
async store({ temp_path, original_name, mime_type }) {
const File = this.models.get('upload::File')
const upload_name = uuid()
await fs.copyFile(temp_path, this.filepath(upload_name))
const f = new File({
original_name,
upload_name,
mime_type,
store: this.config.name,
store_id: upload_name,
})
await f.save()
return f
}
/**
* Send the specified file as the data for the response.
* Sets the appropriate Content-Type and Content-Disposition headers.
* @param {module:flitter-upload/model/File~File} file - the file to send
* @param {express/response} response - the response
* @returns {Promise<void>}
*/
async send_file(file, response) {
const file_path = this.filepath(file.store_id)
response.setHeader('Content-Type', file.mime_type)
response.setHeader('Content-Disposition', `inline; filename="${file.original_name}";`)
response.sendFile(file_path)
}
/**
* Resolve the unique file name of a store file to its absolute path in the store.
* @param {string} file_name - the UUID name of the file
* @returns {string} - absolute path to the stored file
*/
filepath(file_name) {
return path.resolve(this.store_path, file_name)
}
}
module.exports = exports = FlitterStore

58
store/Store.js

@ -1,7 +1,65 @@
/**
* @module flitter-upload/store/Store
*/
const { Injectable } = require('flitter-di')
const ImplementationError = require('libflitter/errors/ImplementationError')
/**
* Abstract class representing a backend for storing and
* retrieving uploaded files.
* @abstract
* @extends module:flitter-di/src/Injectable~Injectable
*/
class Store extends Injectable {
/**
* The configuration for this store.
* @type {object}
*/
config = {}
/**
* Instantiate the store.
* @param {object} config - the store's configuration
*/
constructor(config) {
super()
this.config = config
}
/**
* Initializes the store. Called once when the application is started.
* This is where any logic required to connect to, prepare, or guarantee
* the store is ready to use should occur.
* @returns {Promise<void>}
*/
async init() { }
/**
* Permanently store a temporarily uploaded file in this store.
* @param {object} params
* @param {string} params.temp_path - absolute path to the temporarily uploaded file
* @param {string} params.original_name - the original upload name of the file
* @param {string} params.mime_type - the MIME type of the file.
* @abstract
* @returns {Promise<module:flitter-upload/model/File~File>}
*/
async store({ temp_path, original_name, mime_type }) {
throw new ImplementationError()
}
/**
* Send the specified file as the data for the response.
* This should set the appropriate Content-Type and Content-Disposition headers.
* @param {module:flitter-upload/model/File~File} file - the file to send
* @param {express/response} response - the response
* @abstract
* @returns {Promise<void>}
*/
async send_file(file, response) {
throw new ImplementationError()
}
}
module.exports = exports = Store

23
yarn.lock

@ -0,0 +1,23 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
es6-promisify@^6.0.1:
version "6.0.2"
resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-6.0.2.tgz#525c23725b8510f5f1f2feb5a1fbad93a93e29b4"
integrity sha512-eO6vFm0JvqGzjWIQA6QVKjxpmELfhWbDUWHm1rPfIbn55mhKPiAa5xpLmQWJrNa629ZIeQ8ZvMAi13kvrjK6Mg==
mkdirp@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.3.tgz#4cf2e30ad45959dddea53ad97d518b6c8205e1ea"
integrity sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==
ncp@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
integrity sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=
uuid@^3.3.2:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
Loading…
Cancel
Save