feat: Store image uploads as attachments in database (#1144)

* First pass

* Documentation

* Added optional documentId relationship

* name -> key

* cleanup: No need for separate documentId prop
This commit is contained in:
Tom Moor
2020-01-16 09:42:42 -08:00
committed by GitHub
parent 22230c25e5
commit 8e5a5a57a9
8 changed files with 171 additions and 31 deletions

View File

@@ -10,7 +10,7 @@ import {
makeCredential,
} from '../utils/s3';
import { ValidationError } from '../errors';
import { Event, User, Team } from '../models';
import { Attachment, Event, User, Team } from '../models';
import auth from '../middlewares/authentication';
import pagination from './middlewares/pagination';
import userInviter from '../commands/userInviter';
@@ -76,29 +76,40 @@ router.post('users.update', auth(), async ctx => {
});
router.post('users.s3Upload', auth(), async ctx => {
const { filename, kind, size } = ctx.body;
ctx.assertPresent(filename, 'filename is required');
ctx.assertPresent(kind, 'kind is required');
let { name, filename, documentId, contentType, kind, size } = ctx.body;
// backwards compatability
name = name || filename;
contentType = contentType || kind;
ctx.assertPresent(name, 'name is required');
ctx.assertPresent(contentType, 'contentType is required');
ctx.assertPresent(size, 'size is required');
const { user } = ctx.state;
const s3Key = uuid.v4();
const key = `uploads/${ctx.state.user.id}/${s3Key}/${filename}`;
const key = `uploads/${user.id}/${s3Key}/${name}`;
const credential = makeCredential();
const longDate = format(new Date(), 'YYYYMMDDTHHmmss\\Z');
const policy = makePolicy(credential, longDate);
const endpoint = publicS3Endpoint();
const url = `${endpoint}/${key}`;
await Attachment.create({
key,
size,
url,
contentType,
documentId,
teamId: user.teamId,
userId: user.id,
});
await Event.create({
name: 'user.s3Upload',
data: {
filename,
kind,
size,
url,
},
teamId: ctx.state.user.teamId,
userId: ctx.state.user.id,
data: { name },
teamId: user.teamId,
userId: user.id,
ip: ctx.request.ip,
});
@@ -108,7 +119,7 @@ router.post('users.s3Upload', auth(), async ctx => {
uploadUrl: endpoint,
form: {
'Cache-Control': 'max-age=31557600',
'Content-Type': kind,
'Content-Type': contentType,
acl: 'public-read',
key,
policy,
@@ -118,8 +129,8 @@ router.post('users.s3Upload', auth(), async ctx => {
'x-amz-signature': getSignature(policy),
},
asset: {
contentType: kind,
name: filename,
contentType,
name,
url,
size,
},

View File

@@ -0,0 +1,65 @@
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('attachments', {
id: {
type: Sequelize.UUID,
allowNull: false,
primaryKey: true,
},
teamId: {
type: Sequelize.UUID,
allowNull: false,
references: {
model: 'teams',
},
},
userId: {
type: Sequelize.UUID,
allowNull: false,
references: {
model: 'users',
},
},
documentId: {
type: Sequelize.UUID,
allowNull: true,
references: {
model: 'documents',
},
},
key: {
type: Sequelize.STRING,
allowNull: false,
},
url: {
type: Sequelize.STRING,
allowNull: false,
},
contentType: {
type: Sequelize.STRING,
allowNull: false,
},
size: {
type: Sequelize.BIGINT,
allowNull: false,
},
acl: {
type: Sequelize.STRING,
allowNull: false,
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
},
});
await queryInterface.addIndex('attachments', ['documentId']);
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('attachments');
},
};

View File

@@ -0,0 +1,53 @@
// @flow
import path from 'path';
import { DataTypes, sequelize } from '../sequelize';
const Attachment = sequelize.define(
'attachment',
{
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
key: {
type: DataTypes.STRING,
allowNull: false,
},
url: {
type: DataTypes.STRING,
allowNull: false,
},
contentType: {
type: DataTypes.STRING,
allowNull: false,
},
size: {
type: DataTypes.BIGINT,
allowNull: false,
},
acl: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'public-read',
validate: {
isIn: [['private', 'public-read']],
},
},
},
{
getterMethods: {
name: function() {
return path.parse(this.key).base;
},
},
}
);
Attachment.associate = models => {
Attachment.belongsTo(models.Team);
Attachment.belongsTo(models.Document);
Attachment.belongsTo(models.User);
};
export default Attachment;

View File

@@ -1,5 +1,6 @@
// @flow
import ApiKey from './ApiKey';
import Attachment from './Attachment';
import Authentication from './Authentication';
import Backlink from './Backlink';
import Collection from './Collection';
@@ -18,6 +19,7 @@ import View from './View';
const models = {
ApiKey,
Attachment,
Authentication,
Backlink,
Collection,
@@ -44,6 +46,7 @@ Object.keys(models).forEach(modelName => {
export {
ApiKey,
Attachment,
Authentication,
Backlink,
Collection,

View File

@@ -53,25 +53,29 @@ export default function Api() {
<Description>
You can upload small files and images as part of your documents.
All files are stored using Amazon S3. Instead of uploading files
to Outline, you need to upload them directly to S3 with special
to Outline, you need to upload them directly to S3 with
credentials which can be obtained through this endpoint.
</Description>
<Arguments>
<Argument
id="filename"
description="Filename of the uploaded file"
id="name"
description="Name of the uploaded file"
required
/>
<Argument
id="kind"
description="Mimetype of the document"
id="contentType"
description="Mimetype of the file"
required
/>
<Argument
id="size"
description="Filesize of the document"
description="Size in bytes of the file"
required
/>
<Argument
id="documentId"
description="UUID of the associated document"
/>
</Arguments>
</Method>