Skip to content

Commit 5462174

Browse files
authored
Merge pull request #19 from which-ecosystem/s3-reuploads
S3 reuploads
2 parents c2e1632 + 8f4ae4f commit 5462174

File tree

7 files changed

+146
-10
lines changed

7 files changed

+146
-10
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
/node_modules
2-
/.idea
2+
/.idea
3+
/tmp
4+
.env

hooks/fetchImages.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { HookContext } from '@feathersjs/feathers';
2+
import Bluebird from 'bluebird';
3+
import _ from 'lodash';
4+
5+
6+
export default (paths: string[]) => async (context: HookContext): Promise<HookContext> => {
7+
const {
8+
service,
9+
app,
10+
result,
11+
params: { user }
12+
} = context;
13+
14+
const fileService = app.service('files');
15+
const model = service.Model;
16+
17+
Bluebird.map(paths, async (path: string) => {
18+
const url = _.get(result, path);
19+
20+
// If image is not from our s3, fetch it!
21+
if (!fileService.isS3url(url)) {
22+
const filePath = await fileService.downloadFile(url);
23+
const s3Path = fileService.generateS3Path(user?.username);
24+
const s3Url = await fileService.uploadFileToS3(filePath, s3Path);
25+
return model.findOneAndUpdate({ _id: result._id }, { [path]: s3Url });
26+
}
27+
return url;
28+
});
29+
return context;
30+
};
31+

package-lock.json

Lines changed: 40 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@feathersjs/socketio": "^4.5.4",
2121
"@feathersjs/transport-commons": "^4.5.3",
2222
"@types/aws-sdk": "^2.6.1",
23+
"@types/axios": "^0.14.0",
2324
"@types/bluebird": "^3.5.32",
2425
"@types/cors": "^2.8.6",
2526
"@types/lodash": "^4.14.155",
@@ -29,6 +30,7 @@
2930
"@typescript-eslint/eslint-plugin": "^3.2.0",
3031
"@typescript-eslint/parser": "^3.2.0",
3132
"aws-sdk": "^2.6.1",
33+
"axios": "^0.19.2",
3234
"bluebird": "^3.7.2",
3335
"cors": "^2.8.5",
3436
"feathers-hooks-common": "^5.0.3",

services/files/files.class.ts

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,86 @@
11
import { Application } from '@feathersjs/express';
22
import { Params } from '@feathersjs/feathers';
33
import { v4 } from 'uuid';
4+
import axios from 'axios';
5+
import fs from 'fs';
46

57
// Use require to avoid bug
68
// https://stackoverflow.com/questions/62611373/heroku-crashes-when-importing-aws-sdk
9+
// TODO: use import statement
10+
// eslint-disable-next-line
711
const S3 = require('aws-sdk/clients/s3');
812

913

1014
export default class Files {
11-
app!: Application;
12-
s3!: any;
13-
bucket!: string;
15+
public app!: Application;
16+
17+
private s3!: typeof S3;
18+
19+
private bucket!: string;
1420

1521
async find(params: Params): Promise<string> {
22+
const path = this.generateS3Path(params.user?.username);
23+
return this.getUploadUrl(path);
24+
}
25+
26+
public isS3url(url: string): boolean {
27+
return url.startsWith(`https://${this.bucket}.s3`);
28+
}
29+
30+
public generateS3Path(prefix = '', ext = 'png'): string {
31+
const key = v4();
32+
const fileName = `${key}.${ext}`;
33+
return prefix ? `${prefix}/${fileName}` : fileName;
34+
}
35+
36+
async getUploadUrl(path: string): Promise<string> {
1637
// Return signed upload URL
1738
return this.s3.getSignedUrl('putObject', {
1839
Bucket: this.bucket,
19-
Key: `${params.user?.username}/${v4()}.png`,
40+
Key: path,
2041
ContentType: 'image/*',
21-
Expires: 300,
42+
Expires: 300
2243
});
2344
}
2445

46+
async getDownloadUrl(path: string): Promise<string> {
47+
return this.getUploadUrl(path).then((url: string) => {
48+
const queryIndex = url.indexOf('?');
49+
return url.slice(0, queryIndex);
50+
});
51+
}
52+
53+
private createTmpDir() {
54+
if (!fs.existsSync('tmp')) fs.mkdirSync('tmp');
55+
}
56+
57+
async downloadFile(url: string): Promise<string> {
58+
return new Promise((resolve, reject) => {
59+
this.createTmpDir();
60+
const filePath = `tmp/${v4()}`;
61+
const fileStream = fs.createWriteStream(filePath);
62+
axios.get(url, { responseType: 'stream' })
63+
.then(response => {
64+
response.data.pipe(fileStream)
65+
.on('error', reject)
66+
.on('close', () => resolve(filePath));
67+
})
68+
.catch(error => reject(error));
69+
});
70+
}
71+
72+
async uploadFileToS3(filePath: string, s3Path: string): Promise<string> {
73+
const fileStream = fs.createReadStream(filePath);
74+
await this.s3.upload({
75+
Bucket: this.bucket,
76+
Key: s3Path,
77+
Body: fileStream,
78+
ContentType: 'image/png'
79+
}).promise();
80+
fs.unlinkSync(filePath);
81+
return this.getDownloadUrl(s3Path);
82+
}
83+
2584
setup(app: Application): void {
2685
this.app = app;
2786
this.s3 = new S3({

services/polls/polls.hooks.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { PollSchema } from '../../models/polls/poll.schema';
88
import VoteModel from '../../models/votes/vote.model';
99
import sortByDate from '../../hooks/sortByDate';
1010
import signAuthority from '../../hooks/signAuthority';
11+
import fetchImages from '../../hooks/fetchImages';
1112

1213

1314
const convertPoll = async (context: HookContext): Promise<HookContext> => {
@@ -53,7 +54,8 @@ export default {
5354
patch: disallow('external')
5455
},
5556
after: {
56-
all: convertPoll
57+
all: convertPoll,
58+
create: fetchImages(['contents.left.url', 'contents.right.url'])
5759
}
5860
};
5961

services/users/users.hooks.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { discard, disallow } from 'feathers-hooks-common';
44
import { HookContext } from '@feathersjs/feathers';
55
import { NotAuthenticated } from '@feathersjs/errors';
66
import requireAuth from '../../hooks/requireAuth';
7+
import fetchImages from '../../hooks/fetchImages';
78

89
const hashPassword = hooks.hashPassword('password');
910

@@ -24,6 +25,8 @@ const compareUser = async (context: HookContext): Promise<HookContext> => {
2425
export default {
2526
after: {
2627
all: hooks.protect('password'),
28+
create: fetchImages(['avatarUrl']),
29+
patch: fetchImages(['avatarUrl']),
2730
get: discard('password') // Protect password from local get's
2831
},
2932
before: {

0 commit comments

Comments
 (0)