Skip to content

Commit

Permalink
feat(plugin-media): added imgur and delete functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
tristanMatthias committed Feb 13, 2019
1 parent 14964e8 commit 4eca236
Show file tree
Hide file tree
Showing 11 changed files with 393 additions and 130 deletions.
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
| [packages/admin/src/lib/reduxInjector.ts](packages/admin/src/lib/reduxInjector.ts#L6) | 6 | Fix ts-ignores
| [packages/admin/src/router/Router.ts](packages/admin/src/router/Router.ts#L73) | 73 | Convert to a better structure
| [packages/core-lib/src/Renderer/index.ts](packages/core-lib/src/Renderer/index.ts#L83) | 83 | Remove dependency
| [packages/plugin-media/src/providers/s3.ts](packages/plugin-media/src/providers/s3.ts#L104) | 104 | Investigate if data.LastModified is actually a date
| [packages/plugin-media/src/providers/s3.ts](packages/plugin-media/src/providers/s3.ts#L110) | 110 | Investigate if data.LastModified is actually a date
| [packages/cli/src/commands/new/index.ts](packages/cli/src/commands/new/index.ts#L57) | 57 | Init database
| [packages/core-server/src/lib/App/App.ts](packages/core-server/src/lib/App/App.ts#L187) | 187 | convert to gzip serve
| [packages/admin/src/components/pages/Login/PageLogin.ts](packages/admin/src/components/pages/Login/PageLogin.ts#L66) | 66 | Loading state for login form
Expand Down
1 change: 1 addition & 0 deletions packages/core-lib/src/types/Origami.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ export namespace Origami {
create(resource: Resource): Resource;

find(query: object, opts?: object): Promise<Resource | Resource[] | null>;
delete(idOrObj: string | object): Promise<true | undefined>;
}

export interface Resource {
Expand Down
3 changes: 3 additions & 0 deletions packages/plugin-media/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@
"dependencies": {
"@origami/core": "0.0.3-alpha.14",
"aws-sdk": "^2.337.0",
"color": "^3.1.0",
"mkdir-recursive": "^0.4.0",
"request-progress": "^3.0.0",
"request-promise-native": "^1.0.5",
"sharp": "^0.21.2"
},
"devDependencies": {
Expand Down
24 changes: 21 additions & 3 deletions packages/plugin-media/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { error, Origami, Server } from '@origami/core';
import Media from './models/media';
import * as ProviderFilesystem from './providers/filesystem';
import * as ProviderImgur from './providers/imgur';
import * as ProviderS3 from './providers/s3';

export type ProviderTypes = 's3' | 'filesystem';
export type ProviderTypes = 's3' | 'filesystem' | 'imgur';
export type PluginOptionsBase = {
provider: ProviderTypes;
};
Expand All @@ -20,12 +21,24 @@ export interface PluginOptionsS3 extends PluginOptionsBase {
region: string;
}

export type PluginOptions = PluginOptionsFileSystem | PluginOptionsS3;
export interface PluginOptionsImgur extends PluginOptionsBase {
provider: 'imgur';
clientID: string;
clientSecret: string;
}

export type PluginOptions = PluginOptionsFileSystem | PluginOptionsS3 | PluginOptionsImgur;

export interface Provider<T> {
initialize(options: T): any;
handlerCreate(options: T): Origami.Server.RequestHandler;
handlerGet(options: T): Origami.Server.RequestHandler;
handlerDelete(options: T): Origami.Server.RequestHandler;
}

export interface MediaResource {
id: string;
type: string;
}

// @ts-ignore
Expand Down Expand Up @@ -55,6 +68,10 @@ module.exports = async (server: Server, options: PluginOptions) => {
provider = ProviderFilesystem;
break;

case 'imgur':
provider = ProviderImgur;
break;

default:
throw new Error(
`Media provider ${
Expand All @@ -76,7 +93,8 @@ module.exports = async (server: Server, options: PluginOptions) => {
},
controllers: {
create: provider.handlerCreate(options),
get: provider.handlerGet(options)
get: provider.handlerGet(options),
delete: provider.handlerDelete(options)
}
});
};
66 changes: 66 additions & 0 deletions packages/plugin-media/src/lib/parseImg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Origami } from '@origami/core';
import Color from 'color';
import sharp from 'sharp';
import { Readable } from 'stream';

export const parseImg = (
stream: Readable,
req: Origami.Server.Request,
res: Origami.Server.Response,
next: Origami.Server.NextFunction
) => {
let s = stream;

try {
let _modify = sharp();
_modify.on('error', (err) => {
next(err);
return;
});


let width;
if (req.query.width) width = parseInt(req.query.width);

let height;
if (req.query.height) height = parseInt(req.query.height);

let left;
if (req.query.left) left = parseInt(req.query.left);

let top;
if (req.query.top) top = parseInt(req.query.top);

let fit;
if (req.query.fit) fit = req.query.fit;

let position;
if (req.query.position) position = req.query.position.replace(/-/gm, ' ');

let background = 'black';
if (req.query.bg) background = Color(req.query.bg).hex();

// Crop image
if (top !== undefined && left !== undefined && width !== undefined && height !== undefined) {
_modify = _modify.extract({top, left, width, height});

// Resize image
} else if (width || height) {
const resize: sharp.ResizeOptions = {};
if (fit) resize.fit = fit;
if (position) resize.position = position;
if (background) resize.background = background;

_modify = _modify.resize(width, height, resize);
}


s = s.pipe(_modify);

res.locals.content.set(s);
next();

} catch (e) {
next(e);
}
};
6 changes: 0 additions & 6 deletions packages/plugin-media/src/lib/thumbnail.ts

This file was deleted.

3 changes: 2 additions & 1 deletion packages/plugin-media/src/models/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export default {
name: { type: String, required: true },
type: { type: String, required: true },
provider: { type: String, required: true, default: 'filesystem' },
author: { type: 'uuid', required: false }
author: { type: 'uuid', required: false },
providerInfo: {type: Object, required: false}
}
};
58 changes: 36 additions & 22 deletions packages/plugin-media/src/providers/filesystem.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Origami } from '@origami/core';
import fs from 'fs';
// @ts-ignore
import { mkdir as mkdirAsync } from 'mkdir-recursive';
import path from 'path';
import { Readable } from 'stream';
import { promisify } from 'util';
import { PluginOptionsFileSystem } from '../';
import { thumbnail } from '../lib/thumbnail';
import { MediaResource, PluginOptionsFileSystem } from '../';
import { parseImg } from '../lib/parseImg';

// @ts-ignore
import { mkdir as mkdirAsync } from 'mkdir-recursive';
import { Readable, Stream, Writable } from 'stream';
const writeFile = promisify(fs.writeFile);
const deleteFile = promisify(fs.unlink);
const stat = promisify(fs.stat);
const mkdir = promisify(mkdirAsync);

Expand Down Expand Up @@ -72,10 +73,7 @@ export const handlerGet = (
options: PluginOptionsFileSystem
): Origami.Server.RequestHandler => async (req, res, next) => {
const m = res.app.get('store').model('media');
interface MediaResource {
id: string;
type: string;
}

const file = (await m.find({ id: req.params.mediaId })) as MediaResource;
if (!file) {
res.locals.responseCode = 'resource.errors.notFound';
Expand All @@ -85,24 +83,40 @@ export const handlerGet = (
res.header('content-type', file.type);


let stream: Readable = fs.createReadStream(path.resolve(location, file.id));
const stream: Readable = fs.createReadStream(path.resolve(location, file.id));
stream.on('error', (err) => {
next(err);
return;
});

try {
if (req.query.width) {
const width = parseInt(req.query.width);
if (typeof width === 'number') {
stream = stream.pipe(thumbnail(width)) as Readable;
}
// @ts-ignore Is a stream
parseImg(stream, req, res, next);
};


/**
* Delete a media resource from the filesystem and mark as deleted in DB
*/
export const handlerDelete = (options: PluginOptionsFileSystem): Origami.Server.RequestHandler =>
async (req, res, next) => {
// Lookup file in database first
const m = res.app.get('store').model('media') as Origami.Store.Model;

const file = (await m.find({ id: req.params.mediaId })) as MediaResource;

if (!file) {
next();
return;
}

res.locals.content.set(stream);
next();
await deleteFile(path.resolve(location, file.id));

} catch (e) {
next(e);
}
};

try {
await m.delete(file.id);
res.locals.content.responseCode = 'resource.success.deleted';
next();
} catch (e) {
next(e);
}
};
131 changes: 131 additions & 0 deletions packages/plugin-media/src/providers/imgur.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { Origami } from '@origami/core';
// tslint:disable-next-line match-default-export-name
import request from 'request-promise-native';
import { MediaPostReq, PluginOptionsImgur } from '..';
import { parseImg } from '../lib/parseImg';


export const initialize = async (options: PluginOptionsImgur) => {};


interface MediaResource {
id: string;
type: string;
providerInfo: {
id: string;
deleteHash: string;
};
}


export const handlerCreate = (
options: PluginOptionsImgur
// @ts-ignore
): Origami.Server.RequestHandler => async (req: MediaPostReq, res, next) => {
if (!req.files) {
next(new Error('No files uploaded'));
return;
}

const [, file] = Object.entries(req.files)[0];

// tslint:disable-next-line await-promise
const r = await request({
uri: 'https://api.imgur.com/3/image',
method: 'POST',
headers: {
Authorization: `Client-ID ${options.clientID}`
},

formData: {
image: file.data,
type: 'file'
},
json: true
});

const m = res.app.get('store').model('media');

try {
const data = (await m.create({
name: file.name,
type: file.mimetype,
provider: 'imgur',
author: req.jwt.data.userId,
providerInfo: {
id: r.data.id,
deleteHash: r.data.deletehash
}
})) as { id: string };

res.locals.content.set(data);

next();

} catch (e) {
next(e);
}
};


export const handlerGet = (
options: PluginOptionsImgur
): Origami.Server.RequestHandler => async (req, res, next) => {
const m = res.app.get('store').model('media');


const file = (await m.find({ id: req.params.mediaId })) as MediaResource;
if (!file) {
res.locals.responseCode = 'resource.errors.notFound';
next();
return;
}
res.header('content-type', file.type);


const fileID = file.providerInfo.id;
const fileType = file.type.split('/').pop();
const stream = request(`https://imgur.com/${fileID}.${fileType}`);
stream.catch((err) => {
next(err);
return;
});

// @ts-ignore Is a stream
parseImg(stream, req, res, next);
};


/**
* Delete a media resource from Imgur and mark as deleted in DB
*/
export const handlerDelete = (options: PluginOptionsImgur): Origami.Server.RequestHandler =>
async (req, res, next) => {
// Lookup file in database first
const m = res.app.get('store').model('media') as Origami.Store.Model;

const file = (await m.find({ id: req.params.mediaId })) as MediaResource;

if (!file) {
next();
return;
}

// tslint:disable-next-line await-promise
const r = await request({
uri: `https://api.imgur.com/3/image/${file.providerInfo.deleteHash}`,
method: 'DELETE',
headers: {
Authorization: `Client-ID ${options.clientID}`
},
json: true
});

try {
await m.delete(file.id);
res.locals.content.responseCode = 'resource.success.deleted';
next();
} catch (e) {
next(e);
}
};
Loading

0 comments on commit 4eca236

Please sign in to comment.