mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-28 05:37:34 +03:00
Added local media storage adapter
refs https://linear.app/tryghost/issue/CORE-121/create-a-video-storage-adapter - This is an experimental implementation of video file upload support (audio is yet to follow) - The storage adapter still needs more thinking as it's almost the same as the "LocalStorgeAdapter" that stores images. - Also the output serializer skipped use of url utils in favor of inline implementatoin - this should almost certainly be it's own package
This commit is contained in:
parent
c45afc7f26
commit
5242566252
177
core/server/adapters/storage/LocalMediaStorage.js
Normal file
177
core/server/adapters/storage/LocalMediaStorage.js
Normal file
@ -0,0 +1,177 @@
|
||||
// # Local File System Video Storage module
|
||||
// The (default) module for storing media, using the local file system
|
||||
const serveStatic = require('../../../shared/express').static;
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const Promise = require('bluebird');
|
||||
const moment = require('moment');
|
||||
const config = require('../../../shared/config');
|
||||
const tpl = require('@tryghost/tpl');
|
||||
const logging = require('@tryghost/logging');
|
||||
const errors = require('@tryghost/errors');
|
||||
const constants = require('@tryghost/constants');
|
||||
const urlUtils = require('../../../shared/url-utils');
|
||||
const StorageBase = require('ghost-storage-base');
|
||||
|
||||
const messages = {
|
||||
videoNotFound: 'Video not found',
|
||||
videoNotFoundWithRef: 'Video not found: {video}',
|
||||
cannotReadVideo: 'Could not read video: {video}'
|
||||
};
|
||||
|
||||
class LocalMediaStore extends StorageBase {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.storagePath = config.getContentPath('media');
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the video to storage (the file system)
|
||||
*
|
||||
* @param {Object} video
|
||||
* @param {String} video.name
|
||||
* @param {String} video.type
|
||||
* @param {String} video.path
|
||||
* @param {String} targetDir
|
||||
* @returns {Promise<String>}
|
||||
*/
|
||||
async save(video, targetDir) {
|
||||
let targetFilename;
|
||||
|
||||
// NOTE: the base implementation of `getTargetDir` returns the format this.storagePath/YYYY/MM
|
||||
targetDir = targetDir || this.getTargetDir(this.storagePath);
|
||||
|
||||
const filename = await this.getUniqueFileName(video, targetDir);
|
||||
|
||||
targetFilename = filename;
|
||||
await fs.mkdirs(targetDir);
|
||||
|
||||
await fs.copy(video.path, targetFilename);
|
||||
|
||||
// The src for the video must be in URI format, not a file system path, which in Windows uses \
|
||||
// For local file system storage can use relative path so add a slash
|
||||
const fullUrl = (
|
||||
urlUtils.urlJoin('/',
|
||||
urlUtils.getSubdir(),
|
||||
constants.STATIC_MEDIA_URL_PREFIX,
|
||||
path.relative(this.storagePath, targetFilename))
|
||||
).replace(new RegExp(`\\${path.sep}`, 'g'), '/');
|
||||
|
||||
return fullUrl;
|
||||
}
|
||||
|
||||
exists(fileName, targetDir) {
|
||||
const filePath = path.join(targetDir || this.storagePath, fileName);
|
||||
|
||||
return fs.stat(filePath)
|
||||
.then(() => {
|
||||
return true;
|
||||
})
|
||||
.catch(() => {
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* For some reason send divides the max age number by 1000
|
||||
* Fallthrough: false ensures that if an video isn't found, it automatically 404s
|
||||
* Wrap server static errors
|
||||
*
|
||||
* @returns {serveStaticContent}
|
||||
*/
|
||||
serve() {
|
||||
const {storagePath} = this;
|
||||
|
||||
return function serveStaticContent(req, res, next) {
|
||||
const startedAtMoment = moment();
|
||||
|
||||
return serveStatic(
|
||||
storagePath,
|
||||
{
|
||||
maxAge: constants.ONE_YEAR_MS,
|
||||
fallthrough: false,
|
||||
onEnd: () => {
|
||||
logging.info('LocalMediaStorage.serve', req.path, moment().diff(startedAtMoment, 'ms') + 'ms');
|
||||
}
|
||||
}
|
||||
)(req, res, (err) => {
|
||||
if (err) {
|
||||
if (err.statusCode === 404) {
|
||||
return next(new errors.NotFoundError({
|
||||
message: tpl(messages.videoNotFound),
|
||||
code: 'STATIC_FILE_NOT_FOUND',
|
||||
property: err.path
|
||||
}));
|
||||
}
|
||||
|
||||
if (err.statusCode === 400) {
|
||||
return next(new errors.BadRequestError({err: err}));
|
||||
}
|
||||
|
||||
if (err.statusCode === 403) {
|
||||
return next(new errors.NoPermissionError({err: err}));
|
||||
}
|
||||
|
||||
return next(new errors.GhostError({err: err}));
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Not implemented.
|
||||
* @returns {Promise.<*>}
|
||||
*/
|
||||
delete() {
|
||||
return Promise.reject('not implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads bytes from disk for a target video
|
||||
* - path of target video (without content path!)
|
||||
*
|
||||
* @param options
|
||||
*/
|
||||
read(options) {
|
||||
options = options || {};
|
||||
|
||||
// remove trailing slashes
|
||||
options.path = (options.path || '').replace(/\/$|\\$/, '');
|
||||
|
||||
const targetPath = path.join(this.storagePath, options.path);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(targetPath, (err, bytes) => {
|
||||
if (err) {
|
||||
if (err.code === 'ENOENT' || err.code === 'ENOTDIR') {
|
||||
return reject(new errors.NotFoundError({
|
||||
err: err,
|
||||
message: tpl(messages.videoNotFoundWithRef, {video: options.path})
|
||||
}));
|
||||
}
|
||||
|
||||
if (err.code === 'ENAMETOOLONG') {
|
||||
return reject(new errors.BadRequestError({err: err}));
|
||||
}
|
||||
|
||||
if (err.code === 'EACCES') {
|
||||
return reject(new errors.NoPermissionError({err: err}));
|
||||
}
|
||||
|
||||
return reject(new errors.GhostError({
|
||||
err: err,
|
||||
message: tpl(messages.cannotReadVideo, {video: options.path})
|
||||
}));
|
||||
}
|
||||
|
||||
resolve(bytes);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LocalMediaStore;
|
@ -1,7 +1,7 @@
|
||||
const adapterManager = require('../../services/adapter-manager');
|
||||
|
||||
/**
|
||||
* @param {'images'|'videos'|'audios'} [feature] - name for the "feature" to enable through adapter, e.g.: images or videos storage
|
||||
* @param {'images'|'media'|'files'} [feature] - name for the "feature" to enable through adapter, e.g.: images or media storage
|
||||
* @returns {Object} adapter instance
|
||||
*/
|
||||
function getStorage(feature) {
|
||||
|
@ -23,7 +23,9 @@
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
"active": "LocalFileStorage"
|
||||
"active": "LocalFileStorage",
|
||||
"media": "LocalMediaStorage",
|
||||
"LocalMediaStorage": {}
|
||||
},
|
||||
"scheduling": {
|
||||
"active": "SchedulingDefault"
|
||||
|
@ -32,6 +32,8 @@ const getContentPath = function getContentPath(type) {
|
||||
switch (type) {
|
||||
case 'images':
|
||||
return path.join(this.get('paths:contentPath'), 'images/');
|
||||
case 'media':
|
||||
return path.join(this.get('paths:contentPath'), 'media/');
|
||||
case 'themes':
|
||||
return path.join(this.get('paths:contentPath'), 'themes/');
|
||||
case 'adapters':
|
||||
|
@ -24,11 +24,11 @@ describe('Adapter Manager: options resolver', function () {
|
||||
});
|
||||
|
||||
it('returns adapter configuration based on specified feature', function () {
|
||||
const name = 'storage:videos';
|
||||
const name = 'storage:media';
|
||||
const adapterServiceConfig = {
|
||||
storage: {
|
||||
active: 'cloud-storage',
|
||||
videos: 'local-storage',
|
||||
media: 'local-storage',
|
||||
'cloud-storage': {
|
||||
custom: 'configValue'
|
||||
},
|
||||
@ -48,11 +48,11 @@ describe('Adapter Manager: options resolver', function () {
|
||||
});
|
||||
|
||||
it('returns active configuration if piece of feature adapter is missing', function () {
|
||||
const name = 'storage:videos';
|
||||
const name = 'storage:media';
|
||||
const adapterServiceConfig = {
|
||||
storage: {
|
||||
active: 'cloud-storage',
|
||||
videos: 'local-storage',
|
||||
media: 'local-storage',
|
||||
'cloud-storage': {
|
||||
custom: 'configValue'
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user