Files can be added to templates and managed in a dedicated "Files" view.
Mosaico integration in progress. The files seem to be working for Mosaico.
This commit is contained in:
parent
c85f2d4440
commit
b5cdf57f72
23 changed files with 506 additions and 164 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -31,3 +31,5 @@ public/grapejs/templates/*
|
||||||
config/production.toml
|
config/production.toml
|
||||||
workers/reports/config/production.toml
|
workers/reports/config/production.toml
|
||||||
docker-compose.override.yml
|
docker-compose.override.yml
|
||||||
|
|
||||||
|
/files
|
||||||
|
|
15
app.js
15
app.js
|
@ -30,6 +30,7 @@ const api = require('./routes/api');
|
||||||
const reports = require('./routes/reports');
|
const reports = require('./routes/reports');
|
||||||
const subscription = require('./routes/subscription');
|
const subscription = require('./routes/subscription');
|
||||||
const mosaico = require('./routes/mosaico');
|
const mosaico = require('./routes/mosaico');
|
||||||
|
const files = require('./routes/files');
|
||||||
|
|
||||||
const namespacesRest = require('./routes/rest/namespaces');
|
const namespacesRest = require('./routes/rest/namespaces');
|
||||||
const usersRest = require('./routes/rest/users');
|
const usersRest = require('./routes/rest/users');
|
||||||
|
@ -46,6 +47,7 @@ const subscriptionsRest = require('./routes/rest/subscriptions');
|
||||||
const templatesRest = require('./routes/rest/templates');
|
const templatesRest = require('./routes/rest/templates');
|
||||||
const blacklistRest = require('./routes/rest/blacklist');
|
const blacklistRest = require('./routes/rest/blacklist');
|
||||||
const editorsRest = require('./routes/rest/editors');
|
const editorsRest = require('./routes/rest/editors');
|
||||||
|
const filesRest = require('./routes/rest/files');
|
||||||
|
|
||||||
const root = require('./routes/root');
|
const root = require('./routes/root');
|
||||||
|
|
||||||
|
@ -55,15 +57,11 @@ const app = express();
|
||||||
|
|
||||||
function install404Fallback(url) {
|
function install404Fallback(url) {
|
||||||
app.use(url, (req, res, next) => {
|
app.use(url, (req, res, next) => {
|
||||||
let err = new Error(_('Not Found'));
|
next(new interoperableErrors.NotFoundError());
|
||||||
err.status = 404;
|
|
||||||
next(err);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(url + '/*', (req, res, next) => {
|
app.use(url + '/*', (req, res, next) => {
|
||||||
let err = new Error(_('Not Found'));
|
next(new interoperableErrors.NotFoundError());
|
||||||
err.status = 404;
|
|
||||||
next(err);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -254,17 +252,17 @@ app.use((req, res, next) => {
|
||||||
|
|
||||||
// Regular endpoints
|
// Regular endpoints
|
||||||
useWith404Fallback('/subscription', subscription);
|
useWith404Fallback('/subscription', subscription);
|
||||||
|
useWith404Fallback('/files', files);
|
||||||
|
useWith404Fallback('/mosaico', mosaico);
|
||||||
|
|
||||||
if (config.reports && config.reports.enabled === true) {
|
if (config.reports && config.reports.enabled === true) {
|
||||||
useWith404Fallback('/reports', reports);
|
useWith404Fallback('/reports', reports);
|
||||||
}
|
}
|
||||||
|
|
||||||
useWith404Fallback('/mosaico', mosaico);
|
|
||||||
|
|
||||||
// API endpoints
|
// API endpoints
|
||||||
useWith404Fallback('/api', api);
|
useWith404Fallback('/api', api);
|
||||||
|
|
||||||
/* ------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
// REST endpoints
|
// REST endpoints
|
||||||
app.use('/rest', namespacesRest);
|
app.use('/rest', namespacesRest);
|
||||||
|
@ -280,6 +278,7 @@ app.use('/rest', subscriptionsRest);
|
||||||
app.use('/rest', templatesRest);
|
app.use('/rest', templatesRest);
|
||||||
app.use('/rest', blacklistRest);
|
app.use('/rest', blacklistRest);
|
||||||
app.use('/rest', editorsRest);
|
app.use('/rest', editorsRest);
|
||||||
|
app.use('/rest', filesRest);
|
||||||
|
|
||||||
if (config.reports && config.reports.enabled === true) {
|
if (config.reports && config.reports.enabled === true) {
|
||||||
app.use('/rest', reportTemplatesRest);
|
app.use('/rest', reportTemplatesRest);
|
||||||
|
|
|
@ -3,18 +3,19 @@
|
||||||
import React, {Component} from "react";
|
import React, {Component} from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import {translate} from "react-i18next";
|
import {translate} from "react-i18next";
|
||||||
import {requiresAuthenticatedUser} from "./lib/page";
|
import {requiresAuthenticatedUser} from "./page";
|
||||||
import {ACEEditor, Button, Form, FormSendMethod, withForm} from "./lib/form";
|
import {withErrorHandling} from "./error-handling";
|
||||||
import {withErrorHandling} from "./lib/error-handling";
|
import {Table} from "./table";
|
||||||
import {Table} from "./lib/table";
|
|
||||||
import Dropzone from "react-dropzone";
|
import Dropzone from "react-dropzone";
|
||||||
import {ModalDialog} from "./lib/modals";
|
import {ModalDialog} from "./modals";
|
||||||
import {Icon} from "./lib/bootstrap-components";
|
import {Icon} from "./bootstrap-components";
|
||||||
import axios from './axios';
|
import axios from './axios';
|
||||||
|
import styles from "./styles.scss";
|
||||||
|
import {withPageHelpers} from "./page";
|
||||||
|
|
||||||
@translate()
|
@translate()
|
||||||
@withForm
|
|
||||||
@withErrorHandling
|
@withErrorHandling
|
||||||
|
@withPageHelpers
|
||||||
@requiresAuthenticatedUser
|
@requiresAuthenticatedUser
|
||||||
export default class Files extends Component {
|
export default class Files extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -26,16 +27,18 @@ export default class Files extends Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
const t = props.t;
|
const t = props.t;
|
||||||
|
|
||||||
this.initForm();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
entity: PropTypes.object,
|
entity: PropTypes.object,
|
||||||
entityTypeId: PropTypes.string
|
entityTypeId: PropTypes.string,
|
||||||
|
usePublicDownloadUrls: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
usePublicDownloadUrls: true
|
||||||
|
}
|
||||||
|
|
||||||
getFilesUploadedMessage(response){
|
getFilesUploadedMessage(response){
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
|
@ -56,21 +59,21 @@ export default class Files extends Component {
|
||||||
onDrop(files){
|
onDrop(files){
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
this.setFormStatusMessage('info', t('Uploading {{count}} file(s)', files.length));
|
this.setFlashMessage('info', t('Uploading {{count}} file(s)', files.length));
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
data.append('file', file)
|
data.append('files[]', file)
|
||||||
}
|
}
|
||||||
axios.put(`/rest/files/${this.props.entityTypeId}, ${this.props.entity.id}`, data)
|
axios.post(`/rest/files/${this.props.entityTypeId}/${this.props.entity.id}`, data)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
this.filesTable.refresh();
|
this.filesTable.refresh();
|
||||||
const message = this.getFilesUploadedMessage(res);
|
const message = this.getFilesUploadedMessage(res);
|
||||||
this.setFormStatusMessage('info', message);
|
this.setFlashMessage('info', message);
|
||||||
})
|
})
|
||||||
.catch(res => this.setFormStatusMessage('danger', t('File upload failed: ') + res.message));
|
.catch(res => this.setFlashMessage('danger', t('File upload failed: ') + res.message));
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
this.setFormStatusMessage('info', t('No files to upload'));
|
this.setFlashMessage('info', t('No files to upload'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,16 +91,13 @@ export default class Files extends Component {
|
||||||
await this.hideDeleteFile();
|
await this.hideDeleteFile();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.disableForm();
|
this.setFlashMessage('info', t('Deleting file ...'));
|
||||||
this.setFormStatusMessage('info', t('Deleting file ...'));
|
|
||||||
await axios.delete(`/rest/files/${this.props.entityTypeId}/${fileToDeleteId}`);
|
await axios.delete(`/rest/files/${this.props.entityTypeId}/${fileToDeleteId}`);
|
||||||
this.filesTable.refresh();
|
this.filesTable.refresh();
|
||||||
this.setFormStatusMessage('info', t('File deleted'));
|
this.setFlashMessage('info', t('File deleted'));
|
||||||
this.enableForm();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.filesTable.refresh();
|
this.filesTable.refresh();
|
||||||
this.setFormStatusMessage('danger', t('Delete file failed: ') + err.message);
|
this.setFlashMessage('danger', t('Delete file failed: ') + err.message);
|
||||||
this.enableForm();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,14 +106,21 @@ export default class Files extends Component {
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ data: 1, title: "Name" },
|
{ data: 1, title: "Name" },
|
||||||
{ data: 2, title: "Size" },
|
{ data: 3, title: "Size" },
|
||||||
{
|
{
|
||||||
actions: data => {
|
actions: data => {
|
||||||
|
|
||||||
|
let downloadUrl;
|
||||||
|
if (this.props.usePublicDownloadUrls) {
|
||||||
|
downloadUrl =`/files/${this.props.entityTypeId}/${this.props.entity.id}/${data[2]}`;
|
||||||
|
} else {
|
||||||
|
downloadUrl =`/rest/files/${this.props.entityTypeId}/${data[0]}`;
|
||||||
|
}
|
||||||
|
|
||||||
const actions = [
|
const actions = [
|
||||||
{
|
{
|
||||||
label: <Icon icon="download" title={t('Download')}/>,
|
label: <Icon icon="download" title={t('Download')}/>,
|
||||||
href: `/rest/files/${this.props.entityTypeId}/${data[0]}`
|
href: downloadUrl
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: <Icon icon="remove" title={t('Delete')}/>,
|
label: <Icon icon="remove" title={t('Delete')}/>,
|
||||||
|
@ -138,10 +145,10 @@ export default class Files extends Component {
|
||||||
]}>
|
]}>
|
||||||
{t('Are you sure you want to delete file "{{name}}"?', {name: this.state.fileToDeleteName})}
|
{t('Are you sure you want to delete file "{{name}}"?', {name: this.state.fileToDeleteName})}
|
||||||
</ModalDialog>
|
</ModalDialog>
|
||||||
<Dropzone onDrop={::this.onDrop} className="dropZone" activeClassName="dropZoneActive">
|
<Dropzone onDrop={::this.onDrop} className={styles.dropZone} activeClassName="dropZoneActive">
|
||||||
{state => state.isDragActive ? t('Drop {{count}} file(s)', {count:state.draggedFiles.length}) : t('Drop files here')}
|
{state => state.isDragActive ? t('Drop {{count}} file(s)', {count:state.draggedFiles.length}) : t('Drop files here')}
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
<Table withHeader ref={node => this.filesTable = node} dataUrl={`/rest/template-files-table/${this.props.entity.id}`} columns={columns} />
|
<Table withHeader ref={node => this.filesTable = node} dataUrl={`/rest/files-table/${this.props.entityTypeId}/${this.props.entity.id}`} columns={columns} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,3 +87,16 @@
|
||||||
.secondaryNav > li > a {
|
.secondaryNav > li > a {
|
||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropZone {
|
||||||
|
padding-top: 30px;
|
||||||
|
padding-bottom: 30px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
margin-top: 8px;
|
||||||
|
border: 2px solid #E6E9ED;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #FAFAD2;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #808080;
|
||||||
|
}
|
||||||
|
|
|
@ -8,20 +8,41 @@ import {
|
||||||
} from 'react-i18next';
|
} from 'react-i18next';
|
||||||
import i18n from '../lib/i18n';
|
import i18n from '../lib/i18n';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
import styles from "./styles.scss";
|
||||||
|
|
||||||
|
const ResourceType = {
|
||||||
|
TEMPLATE: 'template',
|
||||||
|
CAMPAIGN: 'campaign'
|
||||||
|
}
|
||||||
|
|
||||||
@translate()
|
@translate()
|
||||||
class MosaicoEditor extends Component {
|
class MosaicoEditor extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
this.viewModel = null;
|
||||||
|
this.state = {
|
||||||
|
entityTypeId: ResourceType.TEMPLATE, // FIXME
|
||||||
|
entityId: 13 // FIXME
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
//structure: PropTypes.oneOfType([PropTypes.object, PropTypes.func]).isRequired,
|
//structure: PropTypes.oneOfType([PropTypes.object, PropTypes.func]).isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async onClose(evt) {
|
||||||
|
const t = this.props.t;
|
||||||
|
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
|
||||||
|
if (confirm(t('Unsaved changes will be lost. Close now?'))) {
|
||||||
|
window.location.href = `/${this.state.entityTypeId}s/${this.state.entityId}/edit`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const basePath = '/public/mosaico';
|
const publicPath = '/public/mosaico';
|
||||||
|
|
||||||
if (!Mosaico.isCompatible()) {
|
if (!Mosaico.isCompatible()) {
|
||||||
alert('Update your browser!');
|
alert('Update your browser!');
|
||||||
|
@ -30,35 +51,57 @@ class MosaicoEditor extends Component {
|
||||||
|
|
||||||
const plugins = window.mosaicoPlugins;
|
const plugins = window.mosaicoPlugins;
|
||||||
|
|
||||||
|
plugins.push(viewModel => {
|
||||||
|
this.viewModel = viewModel;
|
||||||
|
});
|
||||||
|
|
||||||
|
// (Custom) HTML postRenderers
|
||||||
|
plugins.push(viewModel => {
|
||||||
|
viewModel.originalExportHTML = viewModel.exportHTML;
|
||||||
|
viewModel.exportHTML = () => {
|
||||||
|
let html = viewModel.originalExportHTML();
|
||||||
|
for (const portRender of window.mosaicoHTMLPostRenderers) {
|
||||||
|
html = postRender(html);
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
plugins.unshift(vm => {
|
plugins.unshift(vm => {
|
||||||
// This is a fix for the use of hardcoded path in Mosaico
|
// This is a fix for the use of hardcoded path in Mosaico
|
||||||
vm.logoPath = basePath + '/img/mosaico32.png'
|
vm.logoPath = publicPath + '/img/mosaico32.png'
|
||||||
});
|
});
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
imgProcessorBackend: basePath+'/img/',
|
imgProcessorBackend: `/mosaico/img/${this.state.entityTypeId}/${this.state.entityId}`,
|
||||||
emailProcessorBackend: basePath+'/dl/',
|
emailProcessorBackend: '/mosaico/dl/',
|
||||||
titleToken: "MOSAICO Responsive Email Designer",
|
titleToken: "MOSAICO Responsive Email Designer",
|
||||||
fileuploadConfig: {
|
fileuploadConfig: {
|
||||||
url: basePath+'/upload/'
|
url: `/mosaico/upload/${this.state.entityTypeId}/${this.state.entityId}`
|
||||||
},
|
},
|
||||||
strings: window.mosaicoLanguageStrings
|
strings: window.mosaicoLanguageStrings
|
||||||
};
|
};
|
||||||
|
|
||||||
const metadata = undefined;
|
const metadata = undefined;
|
||||||
const model = undefined;
|
const model = undefined;
|
||||||
const template = basePath + '/templates/versafix-1/index.html';
|
const template = publicPath + '/templates/versafix-1/index.html';
|
||||||
|
|
||||||
Mosaico.start(config, template, metadata, model, plugins);
|
const allPlugins = plugins.concat(window.mosaicoPlugins);
|
||||||
|
|
||||||
|
Mosaico.start(config, template, metadata, model, allPlugins);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const t = this.props.t;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={styles.navbar}>
|
||||||
|
<img className={styles.logo} src="/public/mailtrain-header.png"/>
|
||||||
|
<a className={styles.btn} onClick={::this.onClose}>{t('CLOSE')}</a>
|
||||||
|
<a className={styles.btn}><span></span></a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
35
client/src/mosaico/styles.scss
Normal file
35
client/src/mosaico/styles.scss
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
:global .mo-standalone {
|
||||||
|
top: 34px;
|
||||||
|
bottom: 0px;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
background: #DE4320;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 34px;
|
||||||
|
top: -34px;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 24px;
|
||||||
|
padding: 5px 0 5px 10px;
|
||||||
|
filter: brightness(0) invert(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: block;
|
||||||
|
float: right;
|
||||||
|
width: 150px;
|
||||||
|
line-height: 34px;
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: sans-serif;
|
||||||
|
cursor: pointer;
|
||||||
|
border-left: 1px solid #972E15;
|
||||||
|
}
|
|
@ -64,6 +64,13 @@ export default class List extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (perms.includes('manageFiles')) {
|
||||||
|
actions.push({
|
||||||
|
label: <Icon icon="hdd" title={t('Files')}/>,
|
||||||
|
link: `/templates/${data[0]}/files`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (perms.includes('share')) {
|
if (perms.includes('share')) {
|
||||||
actions.push({
|
actions.push({
|
||||||
label: <Icon icon="share-alt" title={t('Share')}/>,
|
label: <Icon icon="share-alt" title={t('Share')}/>,
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { Section } from '../lib/page';
|
||||||
import TemplatesCUD from './CUD';
|
import TemplatesCUD from './CUD';
|
||||||
import TemplatesList from './List';
|
import TemplatesList from './List';
|
||||||
import Share from '../shares/Share';
|
import Share from '../shares/Share';
|
||||||
|
import Files from "../lib/files";
|
||||||
|
|
||||||
|
|
||||||
function getMenus(t) {
|
function getMenus(t) {
|
||||||
|
@ -31,6 +32,12 @@ function getMenus(t) {
|
||||||
visible: resolved => resolved.template.permissions.includes('edit'),
|
visible: resolved => resolved.template.permissions.includes('edit'),
|
||||||
panelRender: props => <TemplatesCUD action={props.match.params.action} entity={props.resolved.template} />
|
panelRender: props => <TemplatesCUD action={props.match.params.action} entity={props.resolved.template} />
|
||||||
},
|
},
|
||||||
|
files: {
|
||||||
|
title: t('Files'),
|
||||||
|
link: params => `/templates/${params.templateId}/files`,
|
||||||
|
visible: resolved => resolved.template.permissions.includes('edit'),
|
||||||
|
panelRender: props => <Files title={t('Files')} entity={props.resolved.template} entityTypeId="template" />
|
||||||
|
},
|
||||||
share: {
|
share: {
|
||||||
title: t('Share'),
|
title: t('Share'),
|
||||||
link: params => `/templates/${params.templateId}/share`,
|
link: params => `/templates/${params.templateId}/share`,
|
||||||
|
|
|
@ -15,7 +15,7 @@ module.exports = {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.(js|jsx)$/,
|
test: /\.(js|jsx)$/,
|
||||||
exclude: /(disposables|react-dnd-touch-backend)/ /* https://github.com/react-dnd/react-dnd/issues/407 */,
|
exclude: /(disposables|react-dnd-touch-backend|attr-accept)/ /* https://github.com/react-dnd/react-dnd/issues/407 */,
|
||||||
use: [ 'babel-loader' ]
|
use: [ 'babel-loader' ]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -208,8 +208,8 @@ permissions=["view", "edit", "delete", "share", "createNamespace", "createList",
|
||||||
[roles.namespace.master.children]
|
[roles.namespace.master.children]
|
||||||
list=["view", "edit", "delete", "share", "manageFields", "viewSubscriptions", "manageSubscriptions", "manageSegments"]
|
list=["view", "edit", "delete", "share", "manageFields", "viewSubscriptions", "manageSubscriptions", "manageSegments"]
|
||||||
customForm=["view", "edit", "delete", "share"]
|
customForm=["view", "edit", "delete", "share"]
|
||||||
campaign=["view", "edit", "delete", "share"]
|
campaign=["view", "edit", "delete", "share", "manageFiles"]
|
||||||
template=["view", "edit", "delete", "share"]
|
template=["view", "edit", "delete", "share", "manageFiles"]
|
||||||
report=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
|
report=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
|
||||||
reportTemplate=["view", "edit", "delete", "share", "execute"]
|
reportTemplate=["view", "edit", "delete", "share", "execute"]
|
||||||
namespace=["view", "edit", "delete", "share", "createNamespace", "createList", "createCustomForm", "createReport", "createReportTemplate", "createTemplate", "manageUsers"]
|
namespace=["view", "edit", "delete", "share", "createNamespace", "createList", "createCustomForm", "createReport", "createReportTemplate", "createTemplate", "manageUsers"]
|
||||||
|
@ -227,12 +227,12 @@ permissions=["view", "edit", "delete", "share"]
|
||||||
[roles.campaign.master]
|
[roles.campaign.master]
|
||||||
name="Master"
|
name="Master"
|
||||||
description="All permissions"
|
description="All permissions"
|
||||||
permissions=["view", "edit", "delete", "share"]
|
permissions=["view", "edit", "delete", "share", "manageFiles"]
|
||||||
|
|
||||||
[roles.template.master]
|
[roles.template.master]
|
||||||
name="Master"
|
name="Master"
|
||||||
description="All permissions"
|
description="All permissions"
|
||||||
permissions=["view", "edit", "delete", "share"]
|
permissions=["view", "edit", "delete", "share", "manageFiles"]
|
||||||
|
|
||||||
[roles.report.master]
|
[roles.report.master]
|
||||||
name="Master"
|
name="Master"
|
||||||
|
|
|
@ -1,32 +1,21 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const passport = require('./passport');
|
||||||
|
const files = require('../models/files');
|
||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const uploadedFilesDir = path.join(files.filesDir, 'uploaded');
|
||||||
|
|
||||||
function nameToFileName(name) {
|
const multer = require('multer')({
|
||||||
return name.
|
dest: uploadedFilesDir
|
||||||
trim().
|
});
|
||||||
toLowerCase().
|
|
||||||
replace(/[ .+/]/g, '-').
|
function installUploadHandler(router, url, dontReplace = false) {
|
||||||
replace(/[^a-z0-9\-_]/gi, '').
|
router.postAsync(url, passport.loggedIn, multer.array('files[]'), async (req, res) => {
|
||||||
replace(/--*/g, '-');
|
return res.json(await files.createFiles(req.context, req.params.type, req.params.entityId, req.files, dontReplace));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getReportFileBase(report) {
|
|
||||||
return path.join(__dirname, '..', 'protected', 'reports', report.id + '-' + nameToFileName(report.name));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getReportContentFile(report) {
|
|
||||||
return getReportFileBase(report) + '.out';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getReportOutputFile(report) {
|
|
||||||
return getReportFileBase(report) + '.err';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getReportContentFile,
|
installUploadHandler
|
||||||
getReportOutputFile,
|
|
||||||
nameToFileName
|
|
||||||
};
|
};
|
32
lib/report-helpers.js
Normal file
32
lib/report-helpers.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
function nameToFileName(name) {
|
||||||
|
return name.
|
||||||
|
trim().
|
||||||
|
toLowerCase().
|
||||||
|
replace(/[ .+/]/g, '-').
|
||||||
|
replace(/[^a-z0-9\-_]/gi, '').
|
||||||
|
replace(/--*/g, '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getReportFileBase(report) {
|
||||||
|
return path.join(__dirname, '..', 'protected', 'reports', report.id + '-' + nameToFileName(report.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReportContentFile(report) {
|
||||||
|
return getReportFileBase(report) + '.out';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReportOutputFile(report) {
|
||||||
|
return getReportFileBase(report) + '.err';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getReportContentFile,
|
||||||
|
getReportOutputFile,
|
||||||
|
nameToFileName
|
||||||
|
};
|
127
models/files.js
127
models/files.js
|
@ -6,37 +6,49 @@ const dtHelpers = require('../lib/dt-helpers');
|
||||||
const shares = require('./shares');
|
const shares = require('./shares');
|
||||||
const fs = require('fs-extra-promise');
|
const fs = require('fs-extra-promise');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const interoperableErrors = require('../shared/interoperable-errors');
|
||||||
|
|
||||||
const filesDir = path.join(__dirname, '..', 'files');
|
const filesDir = path.join(__dirname, '..', 'files');
|
||||||
|
|
||||||
const permittedTypes = new Set(['template']);
|
const permittedTypes = new Set(['template']);
|
||||||
|
|
||||||
function getFilePath(type, entityId, filename) {
|
function getFilePath(type, entityId, filename) {
|
||||||
return path.join(path.join(filesDir, type, id.toString()), filename);
|
return path.join(path.join(filesDir, type, entityId.toString()), filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFilesTable(type) {
|
function getFilesTable(type) {
|
||||||
return 'files_' + type;
|
return 'files_' + type;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listFilesDTAjax(context, type, entityId, params) {
|
async function listDTAjax(context, type, entityId, params) {
|
||||||
enforce(permittedTypes.has(type));
|
enforce(permittedTypes.has(type));
|
||||||
await shares.enforceEntityPermission(context, type, entityId, 'edit');
|
await shares.enforceEntityPermission(context, type, entityId, 'manageFiles');
|
||||||
return await dtHelpers.ajaxList(
|
return await dtHelpers.ajaxList(
|
||||||
params,
|
params,
|
||||||
builder => builder.from(getFilesTable(type)).where({entity: entityId}),
|
builder => builder.from(getFilesTable(type)).where({entity: entityId}),
|
||||||
['id', 'originalname', 'size', 'created']
|
['id', 'originalname', 'filename', 'size', 'created']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function list(context, type, entityId) {
|
||||||
|
return await knex.transaction(async tx => {
|
||||||
|
await shares.enforceEntityPermission(context, type, entityId, 'view');
|
||||||
|
return await tx(getFilesTable(type)).where({entity: entityId}).select(['id', 'originalname', 'filename', 'size', 'created']).orderBy('originalname', 'asc');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function getFileById(context, type, id) {
|
async function getFileById(context, type, id) {
|
||||||
enforce(permittedTypes.has(type));
|
enforce(permittedTypes.has(type));
|
||||||
const file = await knex.transaction(async tx => {
|
const file = await knex.transaction(async tx => {
|
||||||
const file = await knex(getFilesTable(type)).where('id', id).first();
|
const file = await tx(getFilesTable(type)).where('id', id).first();
|
||||||
await shares.enforceEntityPermissionTx(tx, context, type, file.entity, 'edit');
|
await shares.enforceEntityPermissionTx(tx, context, type, file.entity, 'manageFiles');
|
||||||
return file;
|
return file;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
throw new interoperableErrors.NotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mimetype: file.mimetype,
|
mimetype: file.mimetype,
|
||||||
name: file.originalname,
|
name: file.originalname,
|
||||||
|
@ -44,14 +56,18 @@ async function getFileById(context, type, id) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getFileByName(context, type, entityId, name) {
|
async function getFileByFilename(context, type, entityId, name) {
|
||||||
enforce(permittedTypes.has(type));
|
enforce(permittedTypes.has(type));
|
||||||
const file = await knex.transaction(async tx => {
|
const file = await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, type, entityId, 'view');
|
await shares.enforceEntityPermissionTx(tx, context, type, entityId, 'view');
|
||||||
const file = await knex(getFilesTable(type)).where({entity: entityId, originalname: name}).first();
|
const file = await tx(getFilesTable(type)).where({entity: entityId, filename: name}).first();
|
||||||
return file;
|
return file;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
throw new interoperableErrors.NotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mimetype: file.mimetype,
|
mimetype: file.mimetype,
|
||||||
name: file.originalname,
|
name: file.originalname,
|
||||||
|
@ -59,85 +75,114 @@ async function getFileByName(context, type, entityId, name) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createFiles(context, type, entityId, files) {
|
async function createFiles(context, type, entityId, files, dontReplace = false) {
|
||||||
enforce(permittedTypes.has(type));
|
enforce(permittedTypes.has(type));
|
||||||
if (files.length == 0) {
|
if (files.length == 0) {
|
||||||
// No files uploaded
|
// No files uploaded
|
||||||
return {uploaded: 0};
|
return {uploaded: 0};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fileEntities = [];
|
||||||
|
const filesToMove = [];
|
||||||
|
const ignoredFiles = [];
|
||||||
|
const removedFiles = [];
|
||||||
|
const filesRet = [];
|
||||||
|
|
||||||
|
await knex.transaction(async tx => {
|
||||||
|
await shares.enforceEntityPermissionTx(tx, context, type, entityId, 'manageFiles');
|
||||||
|
|
||||||
|
const existingNamesRows = await tx(getFilesTable(type)).where('entity', entityId).select(['filename', 'originalname']);
|
||||||
|
const existingNameMap = new Map();
|
||||||
|
for (const row of existingNamesRows) {
|
||||||
|
existingNameMap.set(row.originalname, row);
|
||||||
|
}
|
||||||
|
|
||||||
const originalNameSet = new Set();
|
const originalNameSet = new Set();
|
||||||
const fileEntities = new Array();
|
|
||||||
const filesToMove = new Array();
|
|
||||||
const ignoredFiles = new Array();
|
|
||||||
|
|
||||||
// Create entities for files
|
// Create entities for files
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (originalNameSet.has(file.originalname)) {
|
const parsedOriginalName = path.parse(file.originalname);
|
||||||
|
let originalName = parsedOriginalName.base;
|
||||||
|
|
||||||
|
if (dontReplace) {
|
||||||
|
let suffix = 1;
|
||||||
|
while (existingNameMap.has(originalName) || originalNameSet.has(originalName)) {
|
||||||
|
originalName = parsedOriginalName.name + '-' + suffix + parsedOriginalName.ext;
|
||||||
|
suffix++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalNameSet.has(originalName)) {
|
||||||
// The file has an original name same as another file
|
// The file has an original name same as another file
|
||||||
ignoredFiles.push(file);
|
ignoredFiles.push(file);
|
||||||
} else {
|
|
||||||
originalNameSet.add(file.originalname);
|
|
||||||
|
|
||||||
const fileEntity = {
|
} else {
|
||||||
|
filesToMove.push(file);
|
||||||
|
|
||||||
|
fileEntities.push({
|
||||||
entity: entityId,
|
entity: entityId,
|
||||||
filename: file.filename,
|
filename: file.filename,
|
||||||
originalname: file.originalname,
|
originalname: originalName,
|
||||||
mimetype: file.mimetype,
|
mimetype: file.mimetype,
|
||||||
encoding: file.encoding,
|
encoding: file.encoding,
|
||||||
size: file.size
|
size: file.size
|
||||||
};
|
});
|
||||||
|
|
||||||
fileEntities.push(fileEntity);
|
filesRet.push({
|
||||||
filesToMove.push(file);
|
name: file.filename,
|
||||||
|
originalName: originalName,
|
||||||
|
size: file.size,
|
||||||
|
type: file.mimetype,
|
||||||
|
url: `/files/${type}/${entityId}/${file.filename}`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingNameMap.has(originalName)) {
|
||||||
|
removedFiles.push(existingNameMap.get(originalName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
originalNameSet.add(originalName);
|
||||||
|
}
|
||||||
|
|
||||||
const originalNameArray = Array.from(originalNameSet);
|
const originalNameArray = Array.from(originalNameSet);
|
||||||
|
|
||||||
const removedFiles = await knex.transaction(async tx => {
|
|
||||||
await shares.enforceEntityPermissionTx(tx, context, type, entityId, 'edit');
|
|
||||||
const removedFiles = await knex(getFilesTable(type)).where('entity', entityId).whereIn('originalname', originalNameArray).select(['filename', 'originalname']);
|
|
||||||
await knex(getFilesTable(type)).where('entity', entityId).whereIn('originalname', originalNameArray).del();
|
await knex(getFilesTable(type)).where('entity', entityId).whereIn('originalname', originalNameArray).del();
|
||||||
if(fileEntities){
|
|
||||||
|
if (fileEntities) {
|
||||||
await knex(getFilesTable(type)).insert(fileEntities);
|
await knex(getFilesTable(type)).insert(fileEntities);
|
||||||
}
|
}
|
||||||
return removedFiles;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const removedNameSet = new Set();
|
|
||||||
|
|
||||||
// Move new files from upload directory to files directory
|
// Move new files from upload directory to files directory
|
||||||
for(const file of filesToMove){
|
for (const file of filesToMove) {
|
||||||
const filePath = getFilePath(entityId, file.filename);
|
const filePath = getFilePath(type, entityId, file.filename);
|
||||||
// The names should be unique, so overwrite is disabled
|
// The names should be unique, so overwrite is disabled
|
||||||
// The directory is created if it does not exist
|
// The directory is created if it does not exist
|
||||||
// Empty options argument is passed, otherwise fails
|
// Empty options argument is passed, otherwise fails
|
||||||
await fs.move(file.path, filePath, {});
|
await fs.move(file.path, filePath, {});
|
||||||
}
|
}
|
||||||
// Remove replaced files from files directory
|
// Remove replaced files from files directory
|
||||||
for(const file of removedFiles){
|
for (const file of removedFiles) {
|
||||||
removedNameSet.add(file.originalname);
|
|
||||||
const filePath = getFilePath(type, entityId, file.filename);
|
const filePath = getFilePath(type, entityId, file.filename);
|
||||||
await fs.remove(filePath);
|
await fs.remove(filePath);
|
||||||
}
|
}
|
||||||
// Remove ignored files from upload directory
|
// Remove ignored files from upload directory
|
||||||
for(const file of ignoredFiles){
|
for (const file of ignoredFiles) {
|
||||||
await fs.remove(file.path);
|
await fs.remove(file.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uploaded: files.length,
|
uploaded: files.length,
|
||||||
added: fileEntities.length - removedNameSet.size,
|
added: fileEntities.length - removedFiles.length,
|
||||||
replaced: removedFiles.length,
|
replaced: removedFiles.length,
|
||||||
ignored: ignoredFiles.length
|
ignored: ignoredFiles.length,
|
||||||
|
files: filesRet
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeFile(context, type, id) {
|
async function removeFile(context, type, id) {
|
||||||
const file = await knex.transaction(async tx => {
|
const file = await knex.transaction(async tx => {
|
||||||
const file = await knex(getFilesTable(type)).where('id', id).select('entity', 'filename').first();
|
const file = await tx(getFilesTable(type)).where('id', id).select('entity', 'filename').first();
|
||||||
await shares.enforceEntityPermissionTx(tx, context, type, file.entity, 'edit');
|
await shares.enforceEntityPermissionTx(tx, context, type, file.entity, 'manageFiles');
|
||||||
await tx(getFilesTable(type)).where('id', id).del();
|
await tx(getFilesTable(type)).where('id', id).del();
|
||||||
return {filename: file.filename, entity: file.entity};
|
return {filename: file.filename, entity: file.entity};
|
||||||
});
|
});
|
||||||
|
@ -147,9 +192,11 @@ async function removeFile(context, type, id) {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
listFilesDTAjax,
|
listDTAjax,
|
||||||
|
list,
|
||||||
getFileById,
|
getFileById,
|
||||||
getFileByName,
|
getFileByFilename,
|
||||||
createFiles,
|
createFiles,
|
||||||
removeFile
|
removeFile,
|
||||||
|
filesDir
|
||||||
};
|
};
|
|
@ -414,6 +414,10 @@ async function removeDefaultShares(tx, user) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkGlobalPermission(context, requiredOperations) {
|
function checkGlobalPermission(context, requiredOperations) {
|
||||||
|
if (!context.user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (context.user.admin) { // This handles the getAdminContext() case
|
if (context.user.admin) { // This handles the getAdminContext() case
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -443,6 +447,10 @@ function enforceGlobalPermission(context, requiredOperations) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _checkPermissionTx(tx, context, entityTypeId, entityId, requiredOperations) {
|
async function _checkPermissionTx(tx, context, entityTypeId, entityId, requiredOperations) {
|
||||||
|
if (!context.user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const entityType = permissions.getEntityType(entityTypeId);
|
const entityType = permissions.getEntityType(entityTypeId);
|
||||||
|
|
||||||
if (context.user.admin) { // This handles the getAdminContext() case. In this case we don't check the permission, but just the existence.
|
if (context.user.admin) { // This handles the getAdminContext() case. In this case we don't check the permission, but just the existence.
|
||||||
|
@ -530,12 +538,20 @@ async function enforceTypePermissionTx(tx, context, entityTypeId, requiredOperat
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGlobalPermissions(context) {
|
function getGlobalPermissions(context) {
|
||||||
|
if (!context.user) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
enforce(!context.user.admin, 'getPermissions is not supposed to be called by assumed admin');
|
enforce(!context.user.admin, 'getPermissions is not supposed to be called by assumed admin');
|
||||||
|
|
||||||
return (config.roles.global[context.user.role] || {}).permissions || [];
|
return (config.roles.global[context.user.role] || {}).permissions || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getPermissionsTx(tx, context, entityTypeId, entityId) {
|
async function getPermissionsTx(tx, context, entityTypeId, entityId) {
|
||||||
|
if (!context.user) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
enforce(!context.user.admin, 'getPermissions is not supposed to be called by assumed admin');
|
enforce(!context.user.admin, 'getPermissions is not supposed to be called by assumed admin');
|
||||||
|
|
||||||
const entityType = permissions.getEntityType(entityTypeId);
|
const entityType = permissions.getEntityType(entityTypeId);
|
||||||
|
|
28
package-lock.json
generated
28
package-lock.json
generated
|
@ -2376,6 +2376,34 @@
|
||||||
"universalify": "0.1.1"
|
"universalify": "0.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"fs-extra-promise": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-extra-promise/-/fs-extra-promise-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-tu0azpexDga5X0WNBRt/BcZhPuY=",
|
||||||
|
"requires": {
|
||||||
|
"bluebird": "3.5.1",
|
||||||
|
"fs-extra": "2.1.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"fs-extra": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-2.1.2.tgz",
|
||||||
|
"integrity": "sha1-BGxwFjzvmq1GsOSn+kZ/si1x3jU=",
|
||||||
|
"requires": {
|
||||||
|
"graceful-fs": "4.1.11",
|
||||||
|
"jsonfile": "2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"jsonfile": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz",
|
||||||
|
"integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=",
|
||||||
|
"requires": {
|
||||||
|
"graceful-fs": "4.1.11"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"fs.realpath": {
|
"fs.realpath": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
|
|
|
@ -68,6 +68,7 @@
|
||||||
"faker": "^4.1.0",
|
"faker": "^4.1.0",
|
||||||
"feedparser": "^2.2.7",
|
"feedparser": "^2.2.7",
|
||||||
"fs-extra": "^4.0.2",
|
"fs-extra": "^4.0.2",
|
||||||
|
"fs-extra-promise": "^1.0.1",
|
||||||
"geoip-ultralight": "^0.1.5",
|
"geoip-ultralight": "^0.1.5",
|
||||||
"gettext-parser": "^1.3.0",
|
"gettext-parser": "^1.3.0",
|
||||||
"gm": "^1.23.1",
|
"gm": "^1.23.1",
|
||||||
|
|
13
routes/files.js
Normal file
13
routes/files.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const router = require('../lib/router-async').create();
|
||||||
|
const files = require('../models/files');
|
||||||
|
const contextHelpers = require('../lib/context-helpers');
|
||||||
|
|
||||||
|
router.getAsync('/:type/:entityId/:fileName', async (req, res) => {
|
||||||
|
const file = await files.getFileByFilename(contextHelpers.getAdminContext(), req.params.type, req.params.entityId, req.params.fileName);
|
||||||
|
res.type(file.mimetype);
|
||||||
|
return res.download(file.path, file.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
|
@ -4,16 +4,139 @@ const config = require('config');
|
||||||
const router = require('../lib/router-async').create();
|
const router = require('../lib/router-async').create();
|
||||||
const passport = require('../lib/passport');
|
const passport = require('../lib/passport');
|
||||||
const clientHelpers = require('../lib/client-helpers');
|
const clientHelpers = require('../lib/client-helpers');
|
||||||
|
const gm = require('gm').subClass({
|
||||||
|
imageMagick: true
|
||||||
|
});
|
||||||
|
|
||||||
const bluebird = require('bluebird');
|
const bluebird = require('bluebird');
|
||||||
const fsReadFile = bluebird.promisify(require('fs').readFile);
|
const fsReadFile = bluebird.promisify(require('fs').readFile);
|
||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
|
const files = require('../models/files');
|
||||||
|
const fileHelpers = require('../lib/file-helpers');
|
||||||
|
|
||||||
// FIXME - add authentication by sandboxToken
|
// FIXME - add authentication by sandboxToken
|
||||||
|
|
||||||
|
async function placeholderImage(width, height) {
|
||||||
|
const magick = gm(width, height, '#707070');
|
||||||
|
const streamAsync = bluebird.promisify(magick.stream.bind(magick));
|
||||||
|
|
||||||
router.getAsync('/editor', passport.csrfProtection, async (req, res) => {
|
const size = 40;
|
||||||
|
let x = 0;
|
||||||
|
let y = 0;
|
||||||
|
|
||||||
|
// stripes
|
||||||
|
while (y < height) {
|
||||||
|
magick
|
||||||
|
.fill('#808080')
|
||||||
|
.drawPolygon([x, y], [x + size, y], [x + size * 2, y + size], [x + size * 2, y + size * 2])
|
||||||
|
.drawPolygon([x, y + size], [x + size, y + size * 2], [x, y + size * 2]);
|
||||||
|
x = x + size * 2;
|
||||||
|
if (x > width) {
|
||||||
|
x = 0;
|
||||||
|
y = y + size * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// text
|
||||||
|
magick
|
||||||
|
.fill('#B0B0B0')
|
||||||
|
.fontSize(20)
|
||||||
|
.drawText(0, 0, width + ' x ' + height, 'center');
|
||||||
|
|
||||||
|
const stream = await streamAsync('png');
|
||||||
|
|
||||||
|
return {
|
||||||
|
format: 'png',
|
||||||
|
stream
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resizedImage(src, method, width, height) {
|
||||||
|
const filePath = path.join(__dirname, '..', src);
|
||||||
|
|
||||||
|
const magick = gm(filePath);
|
||||||
|
const streamAsync = bluebird.promisify(magick.stream.bind(magick));
|
||||||
|
const formatAsync = bluebird.promisify(magick.format.bind(magick));
|
||||||
|
|
||||||
|
const format = (await formatAsync()).toLowerCase();
|
||||||
|
|
||||||
|
if (method === 'resize') {
|
||||||
|
magick
|
||||||
|
.autoOrient()
|
||||||
|
.resize(width, height);
|
||||||
|
} else if (method === 'cover') {
|
||||||
|
magick
|
||||||
|
.autoOrient()
|
||||||
|
.resize(width, height + '^')
|
||||||
|
.gravity('Center')
|
||||||
|
.extent(width, height + '>');
|
||||||
|
} else {
|
||||||
|
throw new Error(`Method ${method} not supported`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = await streamAsync();
|
||||||
|
|
||||||
|
return {
|
||||||
|
format,
|
||||||
|
stream
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeSize(val, min, max, defaultVal, allowNull) {
|
||||||
|
if (val === 'null' && allowNull) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
val = Number(val) || defaultVal;
|
||||||
|
val = Math.max(min, val);
|
||||||
|
val = Math.min(max, val);
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.getAsync('/img/:type/:fileId', passport.loggedIn, async (req, res) => {
|
||||||
|
const method = req.query.method;
|
||||||
|
const params = req.query.params;
|
||||||
|
let [width, height] = params.split(',');
|
||||||
|
let image;
|
||||||
|
|
||||||
|
if (method === 'placeholder') {
|
||||||
|
width = sanitizeSize(width, 1, 2048, 600, false);
|
||||||
|
height = sanitizeSize(height, 1, 2048, 300, false);
|
||||||
|
image = await placeholderImage(width, height);
|
||||||
|
} else {
|
||||||
|
width = sanitizeSize(width, 1, 2048, 600, false);
|
||||||
|
height = sanitizeSize(height, 1, 2048, 300, true);
|
||||||
|
image = await resizedImage(req.query.src, method, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.set('Content-Type', 'image/' + image.format);
|
||||||
|
image.stream.pipe(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
fileHelpers.installUploadHandler(router, '/upload/:type/:entityId', true);
|
||||||
|
|
||||||
|
router.getAsync('/upload/:type/:fileId', passport.loggedIn, async (req, res) => {
|
||||||
|
const entries = await files.list(req.context, req.params.type, req.params.fileId);
|
||||||
|
|
||||||
|
const filesOut = [];
|
||||||
|
for (const entry of entries) {
|
||||||
|
filesOut.push({
|
||||||
|
name: entry.originalname,
|
||||||
|
url: `/files/${req.params.type}/${req.params.fileId}/${entry.filename}`,
|
||||||
|
size: entry.size,
|
||||||
|
thumbnailUrl: `/files/${req.params.type}/${req.params.fileId}/${entry.filename}` // TODO - use smaller thumbnails
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
files: filesOut
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
router.getAsync('/editor', passport.csrfProtection, passport.loggedIn, async (req, res) => {
|
||||||
const resourceType = req.query.type;
|
const resourceType = req.query.type;
|
||||||
const resourceId = req.query.id;
|
const resourceId = req.query.id;
|
||||||
|
|
||||||
|
@ -29,15 +152,6 @@ router.getAsync('/editor', passport.csrfProtection, async (req, res) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ????
|
|
||||||
resource.editorName = resource.editorName || 'mosaico';
|
|
||||||
resource.editorData = !resource.editorData ?
|
|
||||||
{
|
|
||||||
template: req.query.template || 'versafix-1'
|
|
||||||
} :
|
|
||||||
JSON.parse(resource.editorData);
|
|
||||||
*/
|
|
||||||
|
|
||||||
res.render('mosaico/root', {
|
res.render('mosaico/root', {
|
||||||
layout: 'mosaico/layout',
|
layout: 'mosaico/layout',
|
||||||
editorConfig: config.mosaico,
|
editorConfig: config.mosaico,
|
||||||
|
@ -47,4 +161,5 @@ router.getAsync('/editor', passport.csrfProtection, async (req, res) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
const passport = require('../lib/passport');
|
const passport = require('../lib/passport');
|
||||||
const _ = require('../lib/translate')._;
|
const _ = require('../lib/translate')._;
|
||||||
const reports = require('../models/reports');
|
const reports = require('../models/reports');
|
||||||
const fileHelpers = require('../lib/file-helpers');
|
const reportHelpers = require('../lib/report-helpers');
|
||||||
const shares = require('../models/shares');
|
const shares = require('../models/shares');
|
||||||
const contextHelpers = require('../lib/context-helpers');
|
const contextHelpers = require('../lib/context-helpers');
|
||||||
|
|
||||||
|
@ -16,11 +16,11 @@ router.getAsync('/:id/download', passport.loggedIn, async (req, res) => {
|
||||||
|
|
||||||
if (report.state == reports.ReportState.FINISHED) {
|
if (report.state == reports.ReportState.FINISHED) {
|
||||||
const headers = {
|
const headers = {
|
||||||
'Content-Disposition': 'attachment;filename=' + fileHelpers.nameToFileName(report.name) + '.csv',
|
'Content-Disposition': 'attachment;filename=' + reportHelpers.nameToFileName(report.name) + '.csv',
|
||||||
'Content-Type': report.mime_type
|
'Content-Type': report.mime_type
|
||||||
};
|
};
|
||||||
|
|
||||||
res.sendFile(fileHelpers.getReportContentFile(report), {headers: headers});
|
res.sendFile(reportHelpers.getReportContentFile(report), {headers: headers});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return res.status(404).send(_('Report not found'));
|
return res.status(404).send(_('Report not found'));
|
||||||
|
|
|
@ -2,13 +2,11 @@
|
||||||
|
|
||||||
const passport = require('../../lib/passport');
|
const passport = require('../../lib/passport');
|
||||||
const files = require('../../models/files');
|
const files = require('../../models/files');
|
||||||
|
|
||||||
const router = require('../../lib/router-async').create();
|
const router = require('../../lib/router-async').create();
|
||||||
const multer = require('../../lib/multer');
|
const fileHelpers = require('../../lib/file-helpers');
|
||||||
|
|
||||||
router.postAsync('/files-table/:type/:entityId', passport.loggedIn, async (req, res) => {
|
router.postAsync('/files-table/:type/:entityId', passport.loggedIn, async (req, res) => {
|
||||||
const files = await files.listFilesDTAjax(req.context, req.params.type, req.params.entityId, req.body);
|
return res.json(await files.listDTAjax(req.context, req.params.type, req.params.entityId, req.body));
|
||||||
return res.json(files);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.getAsync('/files/:type/:fileId', passport.loggedIn, async (req, res) => {
|
router.getAsync('/files/:type/:fileId', passport.loggedIn, async (req, res) => {
|
||||||
|
@ -17,22 +15,11 @@ router.getAsync('/files/:type/:fileId', passport.loggedIn, async (req, res) => {
|
||||||
return res.download(file.path, file.name);
|
return res.download(file.path, file.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.getAsync('/files-by-name/:type/:entityId/:fileName', passport.loggedIn, async (req, res) => {
|
|
||||||
const file = await templates.getFileByName(req.context, req.params.type, req.params.entityId, req.params.fileName);
|
|
||||||
res.type(file.mimetype);
|
|
||||||
// return res.sendFile(file.path); FIXME - remove this comment if the download below is OK
|
|
||||||
return res.download(file.path, file.name);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
router.putAsync('/files/:type/:entityId', passport.loggedIn, multer.array('file'), async (req, res) => {
|
|
||||||
const summary = await files.createFiles(req.context, req.params.type, req.params.entityId, req.files);
|
|
||||||
return res.json(summary);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.deleteAsync('/files/:type/:fileId', passport.loggedIn, async (req, res) => {
|
router.deleteAsync('/files/:type/:fileId', passport.loggedIn, async (req, res) => {
|
||||||
await files.removeFile(req.context, req.params.type, req.params.fileId);
|
await files.removeFile(req.context, req.params.type, req.params.fileId);
|
||||||
return res.json();
|
return res.json();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fileHelpers.installUploadHandler(router, '/files/:type/:entityId');
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
|
@ -4,7 +4,7 @@ const passport = require('../../lib/passport');
|
||||||
const _ = require('../../lib/translate')._;
|
const _ = require('../../lib/translate')._;
|
||||||
const reports = require('../../models/reports');
|
const reports = require('../../models/reports');
|
||||||
const reportProcessor = require('../../lib/report-processor');
|
const reportProcessor = require('../../lib/report-processor');
|
||||||
const fileHelpers = require('../../lib/file-helpers');
|
const reportHelpers = require('../../lib/report-helpers');
|
||||||
const shares = require('../../models/shares');
|
const shares = require('../../models/shares');
|
||||||
const contextHelpers = require('../../lib/context-helpers');
|
const contextHelpers = require('../../lib/context-helpers');
|
||||||
|
|
||||||
|
@ -62,14 +62,14 @@ router.getAsync('/report-content/:id', async (req, res) => {
|
||||||
await shares.enforceEntityPermission(req.context, 'report', req.params.id, 'viewContent');
|
await shares.enforceEntityPermission(req.context, 'report', req.params.id, 'viewContent');
|
||||||
|
|
||||||
const report = await reports.getByIdWithTemplate(contextHelpers.getAdminContext(), req.params.id);
|
const report = await reports.getByIdWithTemplate(contextHelpers.getAdminContext(), req.params.id);
|
||||||
res.sendFile(fileHelpers.getReportContentFile(report));
|
res.sendFile(reportHelpers.getReportContentFile(report));
|
||||||
});
|
});
|
||||||
|
|
||||||
router.getAsync('/report-output/:id', async (req, res) => {
|
router.getAsync('/report-output/:id', async (req, res) => {
|
||||||
await shares.enforceEntityPermission(req.context, 'report', req.params.id, 'viewOutput');
|
await shares.enforceEntityPermission(req.context, 'report', req.params.id, 'viewOutput');
|
||||||
|
|
||||||
const report = await reports.getByIdWithTemplate(contextHelpers.getAdminContext(), req.params.id);
|
const report = await reports.getByIdWithTemplate(contextHelpers.getAdminContext(), req.params.id);
|
||||||
res.sendFile(fileHelpers.getReportOutputFile(report));
|
res.sendFile(reportHelpers.getReportOutputFile(report));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
that can chroot.
|
that can chroot.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const fileHelpers = require('../lib/file-helpers');
|
const reportHelpers = require('../lib/report-helpers');
|
||||||
const fork = require('child_process').fork;
|
const fork = require('child_process').fork;
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const log = require('npmlog');
|
const log = require('npmlog');
|
||||||
|
@ -111,7 +111,7 @@ process.on('message', msg => {
|
||||||
if (type === 'start-report-processor-worker') {
|
if (type === 'start-report-processor-worker') {
|
||||||
|
|
||||||
const ids = privilegeHelpers.getConfigROUidGid();
|
const ids = privilegeHelpers.getConfigROUidGid();
|
||||||
spawnProcess(msg.tid, path.join(__dirname, '..', 'workers', 'reports', 'report-processor.js'), [msg.data.id], fileHelpers.getReportContentFile(msg.data), fileHelpers.getReportOutputFile(msg.data), path.join(__dirname, '..', 'workers', 'reports'), ids.uid, ids.gid);
|
spawnProcess(msg.tid, path.join(__dirname, '..', 'workers', 'reports', 'report-processor.js'), [msg.data.id], reportHelpers.getReportContentFile(msg.data), reportHelpers.getReportOutputFile(msg.data), path.join(__dirname, '..', 'workers', 'reports'), ids.uid, ids.gid);
|
||||||
|
|
||||||
} else if (type === 'stop-process') {
|
} else if (type === 'stop-process') {
|
||||||
const child = processes[msg.tid];
|
const child = processes[msg.tid];
|
||||||
|
|
|
@ -22,7 +22,8 @@ class ChangedError extends InteroperableError {
|
||||||
|
|
||||||
class NotFoundError extends InteroperableError {
|
class NotFoundError extends InteroperableError {
|
||||||
constructor(msg, data) {
|
constructor(msg, data) {
|
||||||
super('NotFoundError', msg, data);
|
super('NotFoundError', msg || 'Not Found', data);
|
||||||
|
this.status = 404;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,7 +83,7 @@ class NamespaceNotFoundError extends InteroperableError {
|
||||||
|
|
||||||
class PermissionDeniedError extends InteroperableError {
|
class PermissionDeniedError extends InteroperableError {
|
||||||
constructor(msg, data) {
|
constructor(msg, data) {
|
||||||
super('PermissionDeniedError', msg, data);
|
super('PermissionDeniedError', msg || 'Permission Denied', data);
|
||||||
this.status = 403;
|
this.status = 403;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue