diff --git a/client/components/common/nav-header.vue b/client/components/common/nav-header.vue index 6c5cc3ef..4a604ecf 100644 --- a/client/components/common/nav-header.vue +++ b/client/components/common/nav-header.vue @@ -91,7 +91,7 @@ //- LANGUAGES template(v-if='mode === `view` && locales.length > 0') - v-menu(offset-y, bottom, nudge-bottom='30', transition='slide-y-transition') + v-menu(offset-y, bottom, transition='slide-y-transition') template(v-slot:activator='{ on: menu }') v-tooltip(bottom) template(v-slot:activator='{ on: tooltip }') @@ -101,14 +101,14 @@ v-list(nav) template(v-for='(lc, idx) of locales') v-list-item(@click='changeLocale(lc)') - v-list-item-action: v-chip(:color='lc.code === locale ? `blue` : `grey`', small, label, dark) {{lc.code.toUpperCase()}} + v-list-item-action(style='min-width:auto;'): v-chip(:color='lc.code === locale ? `blue` : `grey`', small, label, dark) {{lc.code.toUpperCase()}} v-list-item-title {{lc.name}} v-divider(vertical) //- PAGE ACTIONS template(v-if='isAuthenticated && path && mode !== `edit`') - v-menu(offset-y, bottom, nudge-bottom='30', transition='slide-y-transition') + v-menu(offset-y, bottom, transition='slide-y-transition') template(v-slot:activator='{ on: menu }') v-tooltip(bottom) template(v-slot:activator='{ on: tooltip }') @@ -152,7 +152,7 @@ //- ACCOUNT - v-menu(v-if='isAuthenticated', offset-y, bottom, nudge-bottom='30', min-width='300', transition='slide-y-transition') + v-menu(v-if='isAuthenticated', offset-y, bottom, min-width='300', transition='slide-y-transition') template(v-slot:activator='{ on: menu }') v-tooltip(bottom) template(v-slot:activator='{ on: tooltip }') diff --git a/server/controllers/upload.js b/server/controllers/upload.js index db6d27f3..493f45be 100644 --- a/server/controllers/upload.js +++ b/server/controllers/upload.js @@ -90,7 +90,7 @@ router.post('/u', multer({ ...fileMeta, folderId: folderId, assetPath, - userId: req.user.id + user: req.user }) res.send('ok') }) diff --git a/server/graph/resolvers/asset.js b/server/graph/resolvers/asset.js index 5e9c952d..1e8e6e23 100644 --- a/server/graph/resolvers/asset.js +++ b/server/graph/resolvers/asset.js @@ -123,8 +123,11 @@ module.exports = { event: 'renamed', asset: { ...asset, - sourcePath: assetSourcePath, - destinationPath: assetTargetPath + path: assetSourcePath, + destinationPath: assetTargetPath, + moveAuthorId: context.req.user.id, + moveAuthorName: context.req.user.name, + moveAuthorEmail: context.req.user.email } }) @@ -146,7 +149,7 @@ module.exports = { const asset = await WIKI.models.assets.query().findById(args.id) if (asset) { // Check permissions - const assetPath = asset.getAssetPath() + const assetPath = await asset.getAssetPath() if (!WIKI.auth.checkAccess(context.req.user, ['manage:assets'], { path: assetPath })) { throw new WIKI.Error.AssetDeleteForbidden() } @@ -158,7 +161,13 @@ module.exports = { // Delete from Storage await WIKI.models.storage.assetEvent({ event: 'deleted', - asset + asset: { + ...asset, + path: assetPath, + authorId: context.req.user.id, + authorName: context.req.user.name, + authorEmail: context.req.user.email + } }) return { diff --git a/server/models/assetFolders.js b/server/models/assetFolders.js index e2a32ee9..c15cfba4 100644 --- a/server/models/assetFolders.js +++ b/server/models/assetFolders.js @@ -39,7 +39,7 @@ module.exports = class AssetFolder extends Model { * * @param {Number} folderId Id of the folder */ - static async getHierarchy(folderId) { + static async getHierarchy (folderId) { const hier = await WIKI.models.knex.withRecursive('ancestors', qb => { qb.select('id', 'name', 'slug', 'parentId').from('assetFolders').where('id', folderId).union(sqb => { sqb.select('a.id', 'a.name', 'a.slug', 'a.parentId').from('assetFolders AS a').join('ancestors', 'ancestors.parentId', 'a.id') @@ -48,4 +48,22 @@ module.exports = class AssetFolder extends Model { // The ancestors are from children to grandparents, must reverse for correct path order. return _.reverse(hier) } + + /** + * Get full folder paths + */ + static async getAllPaths () { + const all = await WIKI.models.assetFolders.query() + let folders = {} + all.forEach(fld => { + _.set(folders, fld.id, fld.slug) + let parentId = fld.parentId + while (parentId !== null || parentId > 0) { + const parent = _.find(all, ['id', parentId]) + _.set(folders, fld.id, `${parent.slug}/${_.get(folders, fld.id)}`) + parentId = parent.parentId + } + }) + return folders + } } diff --git a/server/models/assets.js b/server/models/assets.js index ce1dd564..8c6fb1e3 100644 --- a/server/models/assets.js +++ b/server/models/assets.js @@ -96,7 +96,7 @@ module.exports = class Asset extends Model { mime: opts.mimetype, fileSize: opts.size, folderId: opts.folderId, - authorId: opts.userId + authorId: opts.user.id } // Save asset data @@ -119,20 +119,27 @@ module.exports = class Asset extends Model { data: fileBuffer }) } + + // Move temp upload to cache + await fs.move(opts.path, path.join(process.cwd(), `data/cache/${fileHash}.dat`), { overwrite: true }) + + // Add to Storage + if (!opts.skipStorage) { + await WIKI.models.storage.assetEvent({ + event: 'uploaded', + asset: { + ...asset, + path: await asset.getAssetPath(), + data: fileBuffer, + authorId: opts.user.id, + authorName: opts.user.name, + authorEmail: opts.user.email + } + }) + } } catch (err) { WIKI.logger.warn(err) } - - // Move temp upload to cache - await fs.move(opts.path, path.join(process.cwd(), `data/cache/${fileHash}.dat`), { overwrite: true }) - - // Add to Storage - if (!opts.skipStorage) { - await WIKI.models.storage.assetEvent({ - event: 'uploaded', - asset - }) - } } static async getAsset(assetPath, res) { diff --git a/server/modules/storage/disk/storage.js b/server/modules/storage/disk/storage.js index 70cb44b5..f56105ca 100644 --- a/server/modules/storage/disk/storage.js +++ b/server/modules/storage/disk/storage.js @@ -88,12 +88,41 @@ module.exports = { await fs.move(path.join(this.config.path, sourceFilePath), path.join(this.config.path, destinationFilePath), { overwrite: true }) }, + /** + * ASSET UPLOAD + * + * @param {Object} asset Asset to upload + */ + async assetUploaded (asset) { + WIKI.logger.info(`(STORAGE/DISK) Creating new file ${asset.path}...`) + await fs.outputFile(path.join(this.config.path, asset.path), asset.data) + }, + /** + * ASSET DELETE + * + * @param {Object} asset Asset to delete + */ + async assetDeleted (asset) { + WIKI.logger.info(`(STORAGE/DISK) Deleting file ${asset.path}...`) + await fs.remove(path.join(this.config.path, asset.path)) + }, + /** + * ASSET RENAME + * + * @param {Object} asset Asset to rename + */ + async assetRenamed (asset) { + WIKI.logger.info(`(STORAGE/DISK) Renaming file from ${asset.path} to ${asset.destinationPath}...`) + await fs.move(path.join(this.config.path, asset.path), path.join(this.config.path, asset.destinationPath), { overwrite: true }) + }, /** * HANDLERS */ async dump() { WIKI.logger.info(`(STORAGE/DISK) Dumping all content to disk...`) + + // -> Pages await pipeline( WIKI.models.knex.column('path', 'localeCode', 'title', 'description', 'contentType', 'content', 'isPublished', 'updatedAt').select().from('pages').where({ isPrivate: false @@ -105,13 +134,30 @@ module.exports = { if (WIKI.config.lang.code !== page.localeCode) { fileName = `${page.localeCode}/${fileName}` } - WIKI.logger.info(`(STORAGE/DISK) Dumping ${fileName}...`) + WIKI.logger.info(`(STORAGE/DISK) Dumping page ${fileName}...`) const filePath = path.join(this.config.path, fileName) await fs.outputFile(filePath, pageHelper.injectPageMetadata(page), 'utf8') cb() } }) ) + + // -> Assets + const assetFolders = await WIKI.models.assetFolders.getAllPaths() + + await pipeline( + WIKI.models.knex.column('filename', 'folderId', 'data').select().from('assets').join('assetData', 'assets.id', '=', 'assetData.id').stream(), + new stream.Transform({ + objectMode: true, + transform: async (asset, enc, cb) => { + const filename = (asset.folderId && asset.folderId > 0) ? `${_.get(assetFolders, asset.folderId)}/${asset.filename}` : asset.filename + WIKI.logger.info(`(STORAGE/DISK) Dumping asset ${filename}...`) + await fs.outputFile(path.join(this.config.path, filename), asset.data) + cb() + } + }) + ) + WIKI.logger.info('(STORAGE/DISK) All content was dumped to disk successfully.') }, async backup() { diff --git a/server/modules/storage/git/storage.js b/server/modules/storage/git/storage.js index 20e76769..2a470728 100644 --- a/server/modules/storage/git/storage.js +++ b/server/modules/storage/git/storage.js @@ -283,7 +283,7 @@ module.exports = { } await this.git.mv(`./${sourceFilePath}`, `./${destinationFilePath}`) - await this.git.commit(`docs: rename ${page.path} to ${page.destinationPath}`, destinationFilePath, { + await this.git.commit(`docs: rename ${page.path} to ${page.destinationPath}`, [sourceFilePath, destinationFilePath], { '--author': `"${page.moveAuthorName} <${page.moveAuthorEmail}>"` }) }, @@ -295,7 +295,7 @@ module.exports = { async assetUploaded (asset) { WIKI.logger.info(`(STORAGE/GIT) Committing new file ${asset.path}...`) const filePath = path.join(this.repoPath, asset.path) - await fs.outputFile(filePath, asset, 'utf8') + await fs.outputFile(filePath, asset.data, 'utf8') await this.git.add(`./${asset.path}`) await this.git.commit(`docs: upload ${asset.path}`, asset.path, { @@ -321,11 +321,11 @@ module.exports = { * @param {Object} asset Asset to upload */ async assetRenamed (asset) { - WIKI.logger.info(`(STORAGE/GIT) Committing file move from ${asset.sourcePath} to ${asset.destinationPath}...`) + WIKI.logger.info(`(STORAGE/GIT) Committing file move from ${asset.path} to ${asset.destinationPath}...`) - await this.git.mv(`./${asset.sourcePath}`, `./${asset.destinationPath}`) - await this.git.commit(`docs: rename ${asset.sourcePath} to ${asset.destinationPath}`, asset.destinationPath, { - '--author': `"${asset.authorName} <${asset.authorEmail}>"` + await this.git.mv(`./${asset.path}`, `./${asset.destinationPath}`) + await this.git.commit(`docs: rename ${asset.path} to ${asset.destinationPath}`, [asset.path, asset.destinationPath], { + '--author': `"${asset.moveAuthorName} <${asset.moveAuthorEmail}>"` }) }, /** @@ -364,6 +364,7 @@ module.exports = { async syncUntracked() { WIKI.logger.info(`(STORAGE/GIT) Adding all untracked content...`) + // -> Pages await pipeline( WIKI.models.knex.column('path', 'localeCode', 'title', 'description', 'contentType', 'content', 'isPublished', 'updatedAt').select().from('pages').where({ isPrivate: false @@ -375,7 +376,7 @@ module.exports = { if (WIKI.config.lang.namespacing && WIKI.config.lang.code !== page.localeCode) { fileName = `${page.localeCode}/${fileName}` } - WIKI.logger.info(`(STORAGE/GIT) Adding ${fileName}...`) + WIKI.logger.info(`(STORAGE/GIT) Adding page ${fileName}...`) const filePath = path.join(this.repoPath, fileName) await fs.outputFile(filePath, pageHelper.injectPageMetadata(page), 'utf8') await this.git.add(`./${fileName}`) @@ -383,6 +384,23 @@ module.exports = { } }) ) + + // -> Assets + const assetFolders = await WIKI.models.assetFolders.getAllPaths() + + await pipeline( + WIKI.models.knex.column('filename', 'folderId', 'data').select().from('assets').join('assetData', 'assets.id', '=', 'assetData.id').stream(), + new stream.Transform({ + objectMode: true, + transform: async (asset, enc, cb) => { + const filename = (asset.folderId && asset.folderId > 0) ? `${_.get(assetFolders, asset.folderId)}/${asset.filename}` : asset.filename + WIKI.logger.info(`(STORAGE/GIT) Adding asset ${filename}...`) + await fs.outputFile(path.join(this.repoPath, filename), asset.data) + await this.git.add(`./${filename}`) + cb() + } + }) + ) await this.git.commit(`docs: add all untracked content`) WIKI.logger.info('(STORAGE/GIT) All content is now tracked.') }