ghost 4.21.0 → 4.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Gruntfile.js +1 -0
- package/content/themes/casper/assets/built/screen.css +1 -1
- package/content/themes/casper/assets/built/screen.css.map +1 -1
- package/content/themes/casper/assets/css/screen.css +263 -50
- package/content/themes/casper/default.hbs +12 -3
- package/content/themes/casper/index.hbs +25 -23
- package/content/themes/casper/package.json +91 -2
- package/content/themes/casper/partials/post-card.hbs +1 -1
- package/content/themes/casper/post.hbs +18 -14
- package/content/themes/casper/yarn.lock +245 -192
- package/core/boot.js +5 -0
- package/core/bridge.js +14 -0
- package/core/built/assets/{chunk.3.065ee3c3bdf674bd81a4.js → chunk.3.1148677ff3b78e5aeaee.js} +60 -60
- package/core/built/assets/{ghost-dark-1328db4a7dd128305646305a8731bcfe.css → ghost-dark-684ad238e1a858c7cb5be6988de7c6f5.css} +1 -1
- package/core/built/assets/{ghost.min-5abc69c04ad1d5301a857e01009b9c05.css → ghost.min-66e08535f8bb797a8c40e0a2b31f1e9e.css} +1 -1
- package/core/built/assets/{ghost.min-6c546c322127ae6d1d1b0ddbf34be75b.js → ghost.min-efbfb823467b66f4acc66537d033aa55.js} +1742 -1891
- package/core/built/assets/{vendor.min-c6ef90bfd7eff256e10b85583bfe9a74.js → vendor.min-7c8fdd90f7ecd2e94328a07ea3b64608.js} +601 -571
- package/core/frontend/helpers/asset.js +9 -1
- package/core/frontend/helpers/ghost_head.js +13 -1
- package/core/frontend/services/card-assets/index.js +16 -0
- package/core/frontend/services/card-assets/service.js +101 -0
- package/core/frontend/services/theme-engine/config/defaults.json +4 -1
- package/core/frontend/services/theme-engine/config/index.js +1 -1
- package/core/frontend/src/cards/css/bookmark.css +83 -0
- package/core/frontend/src/cards/css/gallery.css +36 -0
- package/core/frontend/src/cards/js/gallery.js +8 -0
- package/core/frontend/web/middleware/serve-public-file.js +10 -1
- package/core/frontend/web/site.js +10 -9
- package/core/server/adapters/storage/LocalImagesStorage.js +50 -0
- package/core/server/adapters/storage/LocalMediaStorage.js +23 -0
- package/core/server/adapters/storage/{LocalFileStorage.js → LocalStorageBase.js} +36 -48
- package/core/server/adapters/storage/index.js +1 -1
- package/core/server/adapters/storage/utils.js +2 -2
- package/core/server/api/canary/index.js +4 -0
- package/core/server/api/canary/media.js +22 -0
- package/core/server/api/canary/redirects.js +1 -6
- package/core/server/api/canary/utils/serializers/input/pages.js +8 -0
- package/core/server/api/canary/utils/serializers/output/index.js +4 -0
- package/core/server/api/canary/utils/serializers/output/media.js +28 -0
- package/core/server/api/canary/utils/validators/input/index.js +4 -0
- package/core/server/api/canary/utils/validators/input/media.js +7 -0
- package/core/server/api/v2/redirects.js +1 -6
- package/core/server/api/v3/members.js +5 -1
- package/core/server/api/v3/redirects.js +1 -6
- package/core/server/data/migrations/utils.js +55 -16
- package/core/server/data/migrations/versions/4.22/01-add-is-launch-complete-setting.js +8 -0
- package/core/server/data/migrations/versions/4.22/02-update-launch-complete-setting-from-user-data.js +39 -0
- package/core/server/data/schema/default-settings.json +8 -0
- package/core/server/frontend/ghost.min.css +1 -1
- package/core/server/lib/image/blog-icon.js +2 -4
- package/core/server/lib/image/image-size.js +1 -1
- package/core/server/services/limits.js +3 -6
- package/core/server/services/mega/template.js +4 -0
- package/core/server/services/offers/service.js +1 -31
- package/core/server/services/redirects/api.js +270 -0
- package/core/server/services/redirects/index.js +27 -12
- package/core/server/services/themes/ThemeStorage.js +5 -5
- package/core/server/web/admin/views/default-prod.html +4 -4
- package/core/server/web/admin/views/default.html +4 -4
- package/core/server/web/api/canary/admin/routes.js +13 -4
- package/core/server/web/api/middleware/upload.js +117 -10
- package/core/server/web/members/app.js +1 -1
- package/core/server/web/shared/middlewares/index.js +0 -4
- package/core/shared/config/defaults.json +3 -1
- package/core/shared/config/helpers.js +2 -0
- package/core/shared/config/overrides.json +8 -0
- package/core/shared/labs.js +5 -3
- package/package.json +14 -13
- package/yarn.lock +875 -851
- package/core/built/assets/img/themes/Editorial-a25a4a34c04dedd858bd5e05ef388b1c.jpg +0 -0
- package/core/built/assets/img/themes/Massively-06edf00108429f7fb8e65f190fba34fe.jpg +0 -0
- package/core/server/services/redirects/settings.js +0 -234
- package/core/server/web/shared/middlewares/custom-redirects.js +0 -128
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Usage: `{{asset "css/screen.css"}}`
|
|
3
3
|
//
|
|
4
4
|
// Returns the path to the specified asset.
|
|
5
|
-
const {metaData} = require('../services/proxy');
|
|
5
|
+
const {metaData, urlUtils} = require('../services/proxy');
|
|
6
6
|
const {SafeString} = require('../services/rendering');
|
|
7
7
|
|
|
8
8
|
const errors = require('@tryghost/errors');
|
|
@@ -22,6 +22,14 @@ module.exports = function asset(path, options) {
|
|
|
22
22
|
message: tpl(messages.pathIsRequired)
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
|
+
if (typeof urlUtils.getSiteUrl() !== 'undefined'
|
|
26
|
+
&& typeof urlUtils.getAdminUrl() !== 'undefined'
|
|
27
|
+
&& urlUtils.getSiteUrl() !== urlUtils.getAdminUrl()) {
|
|
28
|
+
const target = new URL(getAssetUrl(path, hasMinFile), urlUtils.getSiteUrl());
|
|
29
|
+
return new SafeString(
|
|
30
|
+
target.href
|
|
31
|
+
);
|
|
32
|
+
}
|
|
25
33
|
|
|
26
34
|
return new SafeString(
|
|
27
35
|
getAssetUrl(path, hasMinFile)
|
|
@@ -5,12 +5,16 @@
|
|
|
5
5
|
const {metaData, settingsCache, config, blogIcon, urlUtils, labs} = require('../services/proxy');
|
|
6
6
|
const {escapeExpression, SafeString} = require('../services/rendering');
|
|
7
7
|
|
|
8
|
+
// BAD REQUIRE
|
|
9
|
+
// @TODO fix this require
|
|
10
|
+
const cardAssetService = require('../services/card-assets');
|
|
11
|
+
|
|
8
12
|
const logging = require('@tryghost/logging');
|
|
9
13
|
const _ = require('lodash');
|
|
10
14
|
const debug = require('@tryghost/debug')('ghost_head');
|
|
11
15
|
const templateStyles = require('./tpl/styles');
|
|
12
16
|
|
|
13
|
-
const getMetaData = metaData
|
|
17
|
+
const {get: getMetaData, getAssetUrl} = metaData;
|
|
14
18
|
|
|
15
19
|
function writeMetaTag(property, content, type) {
|
|
16
20
|
type = type || property.substring(0, 7) === 'twitter' ? 'name' : 'property';
|
|
@@ -193,6 +197,14 @@ module.exports = function ghost_head(options) { // eslint-disable-line camelcase
|
|
|
193
197
|
if (!_.includes(context, 'amp')) {
|
|
194
198
|
head.push(getMembersHelper(options.data));
|
|
195
199
|
|
|
200
|
+
// @TODO do this in a more "frameworky" way
|
|
201
|
+
if (cardAssetService.hasFile('js')) {
|
|
202
|
+
head.push(`<script async src="${getAssetUrl('public/cards.min.js')}"></script>`);
|
|
203
|
+
}
|
|
204
|
+
if (cardAssetService.hasFile('css')) {
|
|
205
|
+
head.push(`<link rel="stylesheet" type="text/css" href="${getAssetUrl('public/cards.min.css')}">`);
|
|
206
|
+
}
|
|
207
|
+
|
|
196
208
|
if (!_.isEmpty(globalCodeinjection)) {
|
|
197
209
|
head.push(globalCodeinjection);
|
|
198
210
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const debug = require('@tryghost/debug')('card-assets');
|
|
2
|
+
const themeEngine = require('../theme-engine');
|
|
3
|
+
|
|
4
|
+
const CardAssetService = require('./service');
|
|
5
|
+
let cardAssetService = new CardAssetService();
|
|
6
|
+
|
|
7
|
+
const initFn = async () => {
|
|
8
|
+
const cardAssetConfig = themeEngine.getActive().config('card_assets');
|
|
9
|
+
debug('initialising with config', cardAssetConfig);
|
|
10
|
+
|
|
11
|
+
await cardAssetService.load(cardAssetConfig);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
module.exports = cardAssetService;
|
|
15
|
+
|
|
16
|
+
module.exports.init = initFn;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const Minifier = require('@tryghost/minifier');
|
|
2
|
+
const _ = require('lodash');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs').promises;
|
|
5
|
+
|
|
6
|
+
class CardAssetService {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
// @TODO: use our config paths concept
|
|
9
|
+
this.src = options.src || path.join(__dirname, '../../src/cards');
|
|
10
|
+
this.dest = options.dest || path.join(__dirname, '../../public');
|
|
11
|
+
this.minifier = new Minifier({src: this.src, dest: this.dest});
|
|
12
|
+
|
|
13
|
+
if ('config' in options) {
|
|
14
|
+
this.config = options.config;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
this.files = [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
generateGlobs() {
|
|
21
|
+
// CASE: The theme has asked for all card assets to be included by default
|
|
22
|
+
if (this.config === true) {
|
|
23
|
+
return {
|
|
24
|
+
'cards.min.css': 'css/*.css',
|
|
25
|
+
'cards.min.js': 'js/*.js'
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// CASE: the theme has declared an include directive, we should include exactly these assets
|
|
30
|
+
// Include rules take precedence over exclude rules.
|
|
31
|
+
if (_.has(this.config, 'include')) {
|
|
32
|
+
return {
|
|
33
|
+
'cards.min.css': `css/(${this.config.include.join('|')}).css`,
|
|
34
|
+
'cards.min.js': `js/(${this.config.include.join('|')}).js`
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// CASE: the theme has declared an exclude directive, we should include exactly these assets
|
|
39
|
+
if (_.has(this.config, 'exclude')) {
|
|
40
|
+
return {
|
|
41
|
+
'cards.min.css': `css/!(${this.config.exclude.join('|')}).css`,
|
|
42
|
+
'cards.min.js': `js/!(${this.config.exclude.join('|')}).js`
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// CASE: theme has asked that no assets be included
|
|
47
|
+
// CASE: we didn't understand config, don't do anything
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async minify(globs) {
|
|
52
|
+
return await this.minifier.minify(globs);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async clearFiles() {
|
|
56
|
+
this.files = [];
|
|
57
|
+
|
|
58
|
+
// @deprecated switch this to use fs.rm when we drop support for Node v12
|
|
59
|
+
try {
|
|
60
|
+
await fs.unlink(path.join(this.dest, 'cards.min.css'));
|
|
61
|
+
} catch (error) {
|
|
62
|
+
// Don't worry if the file didn't exist
|
|
63
|
+
if (error.code !== 'ENOENT') {
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
await fs.unlink(path.join(this.dest, 'cards.min.js'));
|
|
70
|
+
} catch (error) {
|
|
71
|
+
// Don't worry if the file didn't exist
|
|
72
|
+
if (error.code !== 'ENOENT') {
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
hasFile(type) {
|
|
79
|
+
return this.files.indexOf(`cards.min.${type}`) > -1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A theme can declare which cards it supports, and we'll do the rest
|
|
84
|
+
*
|
|
85
|
+
* @param {Array|boolean} config
|
|
86
|
+
* @returns
|
|
87
|
+
*/
|
|
88
|
+
async load(config) {
|
|
89
|
+
if (config) {
|
|
90
|
+
this.config = config;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
await this.clearFiles();
|
|
94
|
+
|
|
95
|
+
const globs = this.generateGlobs();
|
|
96
|
+
|
|
97
|
+
this.files = await this.minify(globs);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = CardAssetService;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
2
|
const defaultConfig = require('./defaults');
|
|
3
|
-
const allowedKeys = ['posts_per_page', 'image_sizes'];
|
|
3
|
+
const allowedKeys = ['posts_per_page', 'image_sizes', 'card_assets'];
|
|
4
4
|
|
|
5
5
|
module.exports.create = function configLoader(packageJson) {
|
|
6
6
|
let config = _.cloneDeep(defaultConfig);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/* style.css */
|
|
2
|
+
|
|
3
|
+
.kg-bookmark-card {
|
|
4
|
+
width: 100%;
|
|
5
|
+
position: relative;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.kg-bookmark-container {
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-wrap: wrap;
|
|
11
|
+
flex-direction: row-reverse;
|
|
12
|
+
color: currentColor;
|
|
13
|
+
font-family: inherit;
|
|
14
|
+
text-decoration: none;
|
|
15
|
+
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.kg-bookmark-container:hover {
|
|
19
|
+
text-decoration: none;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.kg-bookmark-content {
|
|
23
|
+
flex-basis: 0;
|
|
24
|
+
flex-grow: 999;
|
|
25
|
+
padding: 20px;
|
|
26
|
+
order: 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.kg-bookmark-title {
|
|
30
|
+
font-weight: 600;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.kg-bookmark-metadata,
|
|
34
|
+
.kg-bookmark-description {
|
|
35
|
+
margin-top: .5em;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.kg-bookmark-metadata {
|
|
39
|
+
align-items: center;
|
|
40
|
+
white-space: nowrap;
|
|
41
|
+
overflow: hidden;
|
|
42
|
+
text-overflow: ellipsis;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.kg-bookmark-description {
|
|
46
|
+
display: -webkit-box;
|
|
47
|
+
-webkit-box-orient: vertical;
|
|
48
|
+
-webkit-line-clamp: 2;
|
|
49
|
+
overflow: hidden;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.kg-bookmark-icon {
|
|
53
|
+
display: inline-block;
|
|
54
|
+
width: 1em;
|
|
55
|
+
height: 1em;
|
|
56
|
+
vertical-align: text-bottom;
|
|
57
|
+
margin-right: .5em;
|
|
58
|
+
margin-bottom: .05em;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.kg-bookmark-thumbnail {
|
|
62
|
+
display: flex;
|
|
63
|
+
flex-basis: 24rem;
|
|
64
|
+
flex-grow: 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.kg-bookmark-thumbnail img {
|
|
68
|
+
max-width: 100%;
|
|
69
|
+
height: auto;
|
|
70
|
+
vertical-align: bottom;
|
|
71
|
+
object-fit: cover;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.kg-bookmark-author {
|
|
75
|
+
white-space: nowrap;
|
|
76
|
+
text-overflow: ellipsis;
|
|
77
|
+
overflow: hidden;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.kg-bookmark-publisher::before {
|
|
81
|
+
content: "•";
|
|
82
|
+
margin: 0 .5em;
|
|
83
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
.kg-gallery-card {
|
|
2
|
+
margin: 0 0 1.5em;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.kg-gallery-card figcaption {
|
|
6
|
+
margin: -1.0em 0 1.5em;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.kg-gallery-container {
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
margin: 1.5em auto;
|
|
13
|
+
max-width: 1040px;
|
|
14
|
+
width: 100vw;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.kg-gallery-row {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: row;
|
|
20
|
+
justify-content: center;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.kg-gallery-image img {
|
|
24
|
+
display: block;
|
|
25
|
+
margin: 0;
|
|
26
|
+
width: 100%;
|
|
27
|
+
height: 100%;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.kg-gallery-row:not(:first-of-type) {
|
|
31
|
+
margin: 0.75em 0 0 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.kg-gallery-image:not(:first-of-type) {
|
|
35
|
+
margin: 0 0 0 0.75em;
|
|
36
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
var images = document.querySelectorAll('.kg-gallery-image img');
|
|
2
|
+
images.forEach(function (image) {
|
|
3
|
+
var container = image.closest('.kg-gallery-image');
|
|
4
|
+
var width = image.attributes.width.value;
|
|
5
|
+
var height = image.attributes.height.value;
|
|
6
|
+
var ratio = width / height;
|
|
7
|
+
container.style.flex = ratio + ' 1 0%';
|
|
8
|
+
})
|
|
@@ -7,7 +7,8 @@ const urlUtils = require('../../../shared/url-utils');
|
|
|
7
7
|
const tpl = require('@tryghost/tpl');
|
|
8
8
|
|
|
9
9
|
const messages = {
|
|
10
|
-
imageNotFound: 'Image not found'
|
|
10
|
+
imageNotFound: 'Image not found',
|
|
11
|
+
fileNotFound: 'File not found'
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
function createPublicFileMiddleware(file, type, maxAge) {
|
|
@@ -43,6 +44,14 @@ function createPublicFileMiddleware(file, type, maxAge) {
|
|
|
43
44
|
// modify text files before caching+serving to ensure URL placeholders are transformed
|
|
44
45
|
fs.readFile(filePath, (err, buf) => {
|
|
45
46
|
if (err) {
|
|
47
|
+
// Downgrade to a simple 404 if the file didn't exist
|
|
48
|
+
if (err.code === 'ENOENT') {
|
|
49
|
+
err = new errors.NotFoundError({
|
|
50
|
+
message: tpl(messages.fileNotFound),
|
|
51
|
+
code: 'PUBLIC_FILE_NOT_FOUND',
|
|
52
|
+
property: err.path
|
|
53
|
+
});
|
|
54
|
+
}
|
|
46
55
|
return next(err);
|
|
47
56
|
}
|
|
48
57
|
|
|
@@ -17,11 +17,14 @@ const themeEngine = require('../services/theme-engine');
|
|
|
17
17
|
const themeMiddleware = themeEngine.middleware;
|
|
18
18
|
const membersService = require('../../server/services/members');
|
|
19
19
|
const offersService = require('../../server/services/offers');
|
|
20
|
+
const customRedirects = require('../../server/services/redirects');
|
|
20
21
|
const siteRoutes = require('./routes');
|
|
21
22
|
const shared = require('../../server/web/shared');
|
|
22
23
|
const mw = require('./middleware');
|
|
24
|
+
const labs = require('../../shared/labs');
|
|
23
25
|
|
|
24
26
|
const STATIC_IMAGE_URL_PREFIX = `/${urlUtils.STATIC_IMAGE_URL_PREFIX}`;
|
|
27
|
+
const STATIC_MEDIA_URL_PREFIX = `/${constants.STATIC_MEDIA_URL_PREFIX}`;
|
|
25
28
|
|
|
26
29
|
let router;
|
|
27
30
|
|
|
@@ -89,7 +92,7 @@ module.exports = function setupSiteApp(options = {}) {
|
|
|
89
92
|
|
|
90
93
|
// you can extend Ghost with a custom redirects file
|
|
91
94
|
// see https://github.com/TryGhost/Ghost/issues/7707
|
|
92
|
-
|
|
95
|
+
siteApp.use(customRedirects.middleware);
|
|
93
96
|
|
|
94
97
|
// (Optionally) redirect any requests to /ghost to the admin panel
|
|
95
98
|
siteApp.use(mw.redirectGhostToAdmin());
|
|
@@ -106,16 +109,14 @@ module.exports = function setupSiteApp(options = {}) {
|
|
|
106
109
|
siteApp.use(mw.servePublicFile('public/ghost.css', 'text/css', constants.ONE_HOUR_S));
|
|
107
110
|
siteApp.use(mw.servePublicFile('public/ghost.min.css', 'text/css', constants.ONE_YEAR_S));
|
|
108
111
|
|
|
112
|
+
// Card assets
|
|
113
|
+
siteApp.use(mw.servePublicFile('public/cards.min.css', 'text/css', constants.ONE_YEAR_S));
|
|
114
|
+
siteApp.use(mw.servePublicFile('public/cards.min.js', 'text/js', constants.ONE_YEAR_S));
|
|
115
|
+
|
|
109
116
|
// Serve blog images using the storage adapter
|
|
110
117
|
siteApp.use(STATIC_IMAGE_URL_PREFIX, mw.handleImageSizes, storage.getStorage('images').serve());
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// We do this here, at the top level, because helpers require so much stuff.
|
|
114
|
-
// Moving this to being inside themes, where it probably should be requires the proxy to be refactored
|
|
115
|
-
// Else we end up with circular dependencies
|
|
116
|
-
// themeEngine.loadCoreHelpers();
|
|
117
|
-
// themeEngine.registerHandlebarsHelpers();
|
|
118
|
-
// debug('Helpers done');
|
|
118
|
+
// Serve blog media using the storage adapter
|
|
119
|
+
siteApp.use(STATIC_MEDIA_URL_PREFIX, labs.enabledMiddleware('mediaAPI'), storage.getStorage('media').serve());
|
|
119
120
|
|
|
120
121
|
// Global handling for member session, ensures a member is logged in to the frontend
|
|
121
122
|
siteApp.use(membersService.middleware.loadMemberSession);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// # Local File System Image Storage module
|
|
2
|
+
// The (default) module for storing images, using the local file system
|
|
3
|
+
|
|
4
|
+
const fs = require('fs-extra');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const config = require('../../../shared/config');
|
|
7
|
+
|
|
8
|
+
const urlUtils = require('../../../shared/url-utils');
|
|
9
|
+
const LocalStorageBase = require('./LocalStorageBase');
|
|
10
|
+
|
|
11
|
+
let messages = {
|
|
12
|
+
notFound: 'Image not found',
|
|
13
|
+
notFoundWithRef: 'Image not found: {file}',
|
|
14
|
+
cannotRead: 'Could not read image: {file}'
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
class LocalImagesStorage extends LocalStorageBase {
|
|
18
|
+
constructor() {
|
|
19
|
+
super({
|
|
20
|
+
storagePath: config.getContentPath('images'),
|
|
21
|
+
staticFileURLPrefix: urlUtils.STATIC_IMAGE_URL_PREFIX,
|
|
22
|
+
errorMessages: messages
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Saves a buffer in the targetPath
|
|
28
|
+
* @param {Buffer} buffer is an instance of Buffer
|
|
29
|
+
* @param {String} targetPath path to which the buffer should be written
|
|
30
|
+
* @returns {Promise<String>} a URL to retrieve the data
|
|
31
|
+
*/
|
|
32
|
+
async saveRaw(buffer, targetPath) {
|
|
33
|
+
const storagePath = path.join(this.storagePath, targetPath);
|
|
34
|
+
const targetDir = path.dirname(storagePath);
|
|
35
|
+
|
|
36
|
+
await fs.mkdirs(targetDir);
|
|
37
|
+
await fs.writeFile(storagePath, buffer);
|
|
38
|
+
|
|
39
|
+
// For local file system storage can use relative path so add a slash
|
|
40
|
+
const fullUrl = (
|
|
41
|
+
urlUtils.urlJoin('/', urlUtils.getSubdir(),
|
|
42
|
+
this.staticFileURLPrefix,
|
|
43
|
+
targetPath)
|
|
44
|
+
).replace(new RegExp(`\\${path.sep}`, 'g'), '/');
|
|
45
|
+
|
|
46
|
+
return fullUrl;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = LocalImagesStorage;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// # Local File System Video Storage module
|
|
2
|
+
// The (default) module for storing media, using the local file system
|
|
3
|
+
const config = require('../../../shared/config');
|
|
4
|
+
const constants = require('@tryghost/constants');
|
|
5
|
+
const LocalStorageBase = require('./LocalStorageBase');
|
|
6
|
+
|
|
7
|
+
const messages = {
|
|
8
|
+
notFound: 'Media file not found',
|
|
9
|
+
notFoundWithRef: 'Media file not found: {file}',
|
|
10
|
+
cannotRead: 'Could not read media file: {file}'
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
class LocalMediaStore extends LocalStorageBase {
|
|
14
|
+
constructor() {
|
|
15
|
+
super({
|
|
16
|
+
storagePath: config.getContentPath('media'),
|
|
17
|
+
staticFileURLPrefix: constants.STATIC_MEDIA_URL_PREFIX,
|
|
18
|
+
errorMessages: messages
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = LocalMediaStore;
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
// # Local File
|
|
2
|
-
// The (default) module for storing
|
|
1
|
+
// # Local File Base Storage module
|
|
2
|
+
// The (default) module for storing files using the local file system
|
|
3
3
|
const serveStatic = require('../../../shared/express').static;
|
|
4
4
|
|
|
5
5
|
const fs = require('fs-extra');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const Promise = require('bluebird');
|
|
8
8
|
const moment = require('moment');
|
|
9
|
-
const config = require('../../../shared/config');
|
|
10
9
|
const tpl = require('@tryghost/tpl');
|
|
11
10
|
const logging = require('@tryghost/logging');
|
|
12
11
|
const errors = require('@tryghost/errors');
|
|
@@ -15,68 +14,57 @@ const urlUtils = require('../../../shared/url-utils');
|
|
|
15
14
|
const StorageBase = require('ghost-storage-base');
|
|
16
15
|
|
|
17
16
|
const messages = {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
notFound: 'File not found',
|
|
18
|
+
notFoundWithRef: 'File not found: {file}',
|
|
19
|
+
cannotRead: 'Could not read file: {file}'
|
|
21
20
|
};
|
|
22
21
|
|
|
23
|
-
class
|
|
24
|
-
constructor() {
|
|
25
|
-
super();
|
|
26
|
-
|
|
27
|
-
this.storagePath = config.getContentPath('images');
|
|
28
|
-
}
|
|
29
|
-
|
|
22
|
+
class LocalStorageBase extends StorageBase {
|
|
30
23
|
/**
|
|
31
|
-
*
|
|
32
|
-
* @param {
|
|
33
|
-
* @param {String}
|
|
34
|
-
* @
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} options
|
|
26
|
+
* @param {String} options.storagePath
|
|
27
|
+
* @param {String} [options.staticFileURLPrefix]
|
|
28
|
+
* @param {Object} [options.errorMessages]
|
|
29
|
+
* @param {String} [options.errorMessages.notFound]
|
|
30
|
+
* @param {String} [options.errorMessages.notFoundWithRef]
|
|
31
|
+
* @param {String} [options.errorMessages.cannotRead]
|
|
35
32
|
*/
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const targetDir = path.dirname(storagePath);
|
|
39
|
-
|
|
40
|
-
await fs.mkdirs(targetDir);
|
|
41
|
-
await fs.writeFile(storagePath, buffer);
|
|
42
|
-
|
|
43
|
-
// For local file system storage can use relative path so add a slash
|
|
44
|
-
const fullUrl = (
|
|
45
|
-
urlUtils.urlJoin('/', urlUtils.getSubdir(),
|
|
46
|
-
urlUtils.STATIC_IMAGE_URL_PREFIX,
|
|
47
|
-
targetPath)
|
|
48
|
-
).replace(new RegExp(`\\${path.sep}`, 'g'), '/');
|
|
33
|
+
constructor({storagePath, staticFileURLPrefix, errorMessages}) {
|
|
34
|
+
super();
|
|
49
35
|
|
|
50
|
-
|
|
36
|
+
this.storagePath = storagePath;
|
|
37
|
+
this.staticFileURLPrefix = staticFileURLPrefix;
|
|
38
|
+
this.errorMessages = errorMessages || messages;
|
|
51
39
|
}
|
|
52
40
|
|
|
53
41
|
/**
|
|
54
|
-
* Saves the
|
|
55
|
-
* -
|
|
56
|
-
* - returns a promise which ultimately returns the full url to the uploaded image
|
|
42
|
+
* Saves the file to storage (the file system)
|
|
43
|
+
* - returns a promise which ultimately returns the full url to the uploaded file
|
|
57
44
|
*
|
|
58
|
-
* @param {StorageBase.Image}
|
|
45
|
+
* @param {StorageBase.Image} file
|
|
59
46
|
* @param {String} targetDir
|
|
60
47
|
* @returns {Promise<String>}
|
|
61
48
|
*/
|
|
62
|
-
async save(
|
|
49
|
+
async save(file, targetDir) {
|
|
63
50
|
let targetFilename;
|
|
64
51
|
|
|
65
52
|
// NOTE: the base implementation of `getTargetDir` returns the format this.storagePath/YYYY/MM
|
|
66
53
|
targetDir = targetDir || this.getTargetDir(this.storagePath);
|
|
67
54
|
|
|
68
|
-
const filename = await this.getUniqueFileName(
|
|
55
|
+
const filename = await this.getUniqueFileName(file, targetDir);
|
|
69
56
|
|
|
70
57
|
targetFilename = filename;
|
|
71
58
|
await fs.mkdirs(targetDir);
|
|
72
59
|
|
|
73
|
-
await fs.copy(
|
|
60
|
+
await fs.copy(file.path, targetFilename);
|
|
74
61
|
|
|
75
62
|
// The src for the image must be in URI format, not a file system path, which in Windows uses \
|
|
76
63
|
// For local file system storage can use relative path so add a slash
|
|
77
64
|
const fullUrl = (
|
|
78
|
-
urlUtils.urlJoin('/',
|
|
79
|
-
urlUtils.
|
|
65
|
+
urlUtils.urlJoin('/',
|
|
66
|
+
urlUtils.getSubdir(),
|
|
67
|
+
this.staticFileURLPrefix,
|
|
80
68
|
path.relative(this.storagePath, targetFilename))
|
|
81
69
|
).replace(new RegExp(`\\${path.sep}`, 'g'), '/');
|
|
82
70
|
|
|
@@ -103,7 +91,7 @@ class LocalFileStore extends StorageBase {
|
|
|
103
91
|
* @returns {serveStaticContent}
|
|
104
92
|
*/
|
|
105
93
|
serve() {
|
|
106
|
-
const {storagePath} = this;
|
|
94
|
+
const {storagePath, errorMessages} = this;
|
|
107
95
|
|
|
108
96
|
return function serveStaticContent(req, res, next) {
|
|
109
97
|
const startedAtMoment = moment();
|
|
@@ -114,14 +102,14 @@ class LocalFileStore extends StorageBase {
|
|
|
114
102
|
maxAge: constants.ONE_YEAR_MS,
|
|
115
103
|
fallthrough: false,
|
|
116
104
|
onEnd: () => {
|
|
117
|
-
logging.info('
|
|
105
|
+
logging.info('LocalStorageBase.serve', req.path, moment().diff(startedAtMoment, 'ms') + 'ms');
|
|
118
106
|
}
|
|
119
107
|
}
|
|
120
108
|
)(req, res, (err) => {
|
|
121
109
|
if (err) {
|
|
122
110
|
if (err.statusCode === 404) {
|
|
123
111
|
return next(new errors.NotFoundError({
|
|
124
|
-
message: tpl(
|
|
112
|
+
message: tpl(errorMessages.notFound),
|
|
125
113
|
code: 'STATIC_FILE_NOT_FOUND',
|
|
126
114
|
property: err.path
|
|
127
115
|
}));
|
|
@@ -152,8 +140,8 @@ class LocalFileStore extends StorageBase {
|
|
|
152
140
|
}
|
|
153
141
|
|
|
154
142
|
/**
|
|
155
|
-
* Reads bytes from disk for a target
|
|
156
|
-
* - path of target
|
|
143
|
+
* Reads bytes from disk for a target file
|
|
144
|
+
* - path of target file (without content path!)
|
|
157
145
|
*
|
|
158
146
|
* @param options
|
|
159
147
|
*/
|
|
@@ -171,7 +159,7 @@ class LocalFileStore extends StorageBase {
|
|
|
171
159
|
if (err.code === 'ENOENT' || err.code === 'ENOTDIR') {
|
|
172
160
|
return reject(new errors.NotFoundError({
|
|
173
161
|
err: err,
|
|
174
|
-
message: tpl(
|
|
162
|
+
message: tpl(this.errorMessages.notFoundWithRef, {file: options.path})
|
|
175
163
|
}));
|
|
176
164
|
}
|
|
177
165
|
|
|
@@ -185,7 +173,7 @@ class LocalFileStore extends StorageBase {
|
|
|
185
173
|
|
|
186
174
|
return reject(new errors.GhostError({
|
|
187
175
|
err: err,
|
|
188
|
-
message: tpl(
|
|
176
|
+
message: tpl(this.errorMessages.cannotRead, {file: options.path})
|
|
189
177
|
}));
|
|
190
178
|
}
|
|
191
179
|
|
|
@@ -195,4 +183,4 @@ class LocalFileStore extends StorageBase {
|
|
|
195
183
|
}
|
|
196
184
|
}
|
|
197
185
|
|
|
198
|
-
module.exports =
|
|
186
|
+
module.exports = LocalStorageBase;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const adapterManager = require('../../services/adapter-manager');
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @param {'images'|'
|
|
4
|
+
* @param {'images'|'media'|'files'} [feature] - name for the "feature" to enable through adapter, e.g.: images or media storage
|
|
5
5
|
* @returns {Object} adapter instance
|
|
6
6
|
*/
|
|
7
7
|
function getStorage(feature) {
|
|
@@ -12,7 +12,7 @@ const urlUtils = require('../../../shared/url-utils');
|
|
|
12
12
|
* @description Takes a url or filepath and returns a filepath with is readable
|
|
13
13
|
* for the local file storage.
|
|
14
14
|
*/
|
|
15
|
-
exports.
|
|
15
|
+
exports.getLocalImagesStoragePath = function getLocalImagesStoragePath(imagePath) {
|
|
16
16
|
// The '/' in urlJoin is necessary to add the '/' to `content/images`, if no subdirectory is setup
|
|
17
17
|
const urlRegExp = new RegExp(`^${urlUtils.urlJoin(
|
|
18
18
|
urlUtils.urlFor('home', true),
|
|
@@ -43,7 +43,7 @@ exports.getLocalFileStoragePath = function getLocalFileStoragePath(imagePath) {
|
|
|
43
43
|
*/
|
|
44
44
|
|
|
45
45
|
exports.isLocalImage = function isLocalImage(imagePath) {
|
|
46
|
-
const localImagePath = this.
|
|
46
|
+
const localImagePath = this.getLocalImagesStoragePath(imagePath);
|
|
47
47
|
|
|
48
48
|
if (localImagePath !== imagePath) {
|
|
49
49
|
return true;
|