Skip to content

Commit 9182307

Browse files
authored
Allow for filesystem upload
feature/media uploader
2 parents 09f269d + a4f062c commit 9182307

28 files changed

+9647
-8918
lines changed

.env.example

+5-2
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,14 @@ CLOUDFLARE_REGION="auto"
2929
#EMAIL_FROM_ADDRESS=""
3030
#EMAIL_FROM_NAME=""
3131

32+
# Where will social media icons be saved - local or cloudflare.
33+
STORAGE_PROVIDER="local"
34+
3235
# Your upload directory path if you host your files locally, otherwise Cloudflare will be used.
33-
#UPLOAD_DIRECTORY="/opt/postiz/uploads/"
36+
#UPLOAD_DIRECTORY=""
3437

3538
# Your upload directory path if you host your files locally, otherwise Cloudflare will be used.
36-
#NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY="/opt/postiz/uploads/"
39+
#NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY=""
3740

3841

3942
# Social Media API Settings

apps/backend/src/api/api.module.ts

-12
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,6 @@ const authenticatedController = [
4646
@Module({
4747
imports: [
4848
UploadModule,
49-
...(!!process.env.UPLOAD_DIRECTORY &&
50-
!!process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY
51-
? [
52-
ServeStaticModule.forRoot({
53-
rootPath: process.env.UPLOAD_DIRECTORY,
54-
serveRoot: '/' + process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY,
55-
serveStaticOptions: {
56-
index: false,
57-
},
58-
}),
59-
]
60-
: []),
6149
],
6250
controllers: [
6351
RootController,

apps/backend/src/api/routes/media.controller.ts

+18-8
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@ import handleR2Upload from '@gitroom/nestjs-libraries/upload/r2.uploader';
1010
import { FileInterceptor } from '@nestjs/platform-express';
1111
import { CustomFileValidationPipe } from '@gitroom/nestjs-libraries/upload/custom.upload.validation';
1212
import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service';
13+
import { basename } from 'path';
14+
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
1315

1416
@ApiTags('Media')
1517
@Controller('/media')
1618
export class MediaController {
19+
private storage = UploadFactory.createStorage();
1720
constructor(
1821
private _mediaService: MediaService,
1922
private _subscriptionService: SubscriptionService
@@ -33,19 +36,26 @@ export class MediaController {
3336
return {output: 'data:image/png;base64,' + await this._mediaService.generateImage(prompt, org)};
3437
}
3538

36-
@Post('/upload-simple')
39+
@Post('/upload-server')
3740
@UseInterceptors(FileInterceptor('file'))
3841
@UsePipes(new CustomFileValidationPipe())
42+
async uploadServer(
43+
@GetOrgFromRequest() org: Organization,
44+
@UploadedFile() file: Express.Multer.File
45+
) {
46+
const uploadedFile = await this.storage.uploadFile(file);
47+
const filePath = uploadedFile.path.replace(process.env.UPLOAD_DIRECTORY, basename(process.env.UPLOAD_DIRECTORY));
48+
return this._mediaService.saveFile(org.id, uploadedFile.originalname, filePath);
49+
}
50+
51+
@Post('/upload-simple')
52+
@UseInterceptors(FileInterceptor('file'))
3953
async uploadSimple(
4054
@GetOrgFromRequest() org: Organization,
41-
@UploadedFile('file')
42-
file: Express.Multer.File
55+
@UploadedFile('file') file: Express.Multer.File
4356
) {
44-
const filePath =
45-
file.path.indexOf('http') === 0
46-
? file.path
47-
: file.path.replace(process.env.UPLOAD_DIRECTORY, '');
48-
return this._mediaService.saveFile(org.id, file.originalname, filePath);
57+
const getFile = await this.storage.uploadFile(file);
58+
return this._mediaService.saveFile(org.id, getFile.originalname, getFile.path);
4959
}
5060

5161
@Post('/:endpoint')

apps/frontend/next.config.js

+26-1
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,37 @@ const nextConfig = {
1515
transpilePackages: ['crypto-hash'],
1616
images: {
1717
remotePatterns: [
18+
{
19+
protocol: 'http',
20+
hostname: '**',
21+
},
1822
{
1923
protocol: 'https',
2024
hostname: '**',
2125
},
2226
],
23-
}
27+
},
28+
async redirects() {
29+
return [
30+
{
31+
source: '/api/uploads/:path*',
32+
destination:
33+
process.env.STORAGE_PROVIDER === 'local' ? '/uploads/:path*' : '/404',
34+
permanent: true,
35+
},
36+
];
37+
},
38+
async rewrites() {
39+
return [
40+
{
41+
source: '/uploads/:path*',
42+
destination:
43+
process.env.STORAGE_PROVIDER === 'local'
44+
? '/api/uploads/:path*'
45+
: '/404',
46+
},
47+
];
48+
},
2449
};
2550

2651
const plugins = [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { createReadStream, statSync } from 'fs';
3+
// @ts-ignore
4+
import mime from 'mime';
5+
6+
async function* nodeStreamToIterator(stream: any) {
7+
for await (const chunk of stream) {
8+
yield chunk;
9+
}
10+
}
11+
12+
function iteratorToStream(iterator: any) {
13+
return new ReadableStream({
14+
async pull(controller) {
15+
const { value, done } = await iterator.next();
16+
17+
if (done) {
18+
controller.close();
19+
} else {
20+
controller.enqueue(new Uint8Array(value));
21+
}
22+
},
23+
});
24+
}
25+
26+
export const GET = (
27+
request: NextRequest,
28+
context: { params: { path: string[] } }
29+
) => {
30+
const filePath =
31+
process.env.UPLOAD_DIRECTORY + '/' + context.params.path.join('/');
32+
const response = createReadStream(filePath);
33+
34+
const fileStats = statSync(filePath);
35+
const contentType = mime.getType(filePath) || 'application/octet-stream';
36+
37+
const iterator = nodeStreamToIterator(response);
38+
const webStream = iteratorToStream(iterator);
39+
return new Response(webStream, {
40+
headers: {
41+
'Content-Type': contentType, // Set the appropriate content-type header
42+
'Content-Length': fileStats.size.toString(), // Set the content-length header
43+
'Last-Modified': fileStats.mtime.toUTCString(), // Set the last-modified header
44+
'Cache-Control': 'public, max-age=31536000, immutable', // Example cache-control header
45+
},
46+
});
47+
};

apps/frontend/src/components/media/media.component.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { ChangeEvent, FC, useCallback, useEffect, useState } from 'react';
3+
import { FC, useCallback, useEffect, useState } from 'react';
44
import { Button } from '@gitroom/react/form/button';
55
import useSWR from 'swr';
66
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
@@ -11,7 +11,6 @@ import EventEmitter from 'events';
1111
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
1212
import clsx from 'clsx';
1313
import { VideoFrame } from '@gitroom/react/helpers/video.frame';
14-
import { useToaster } from '@gitroom/react/toaster/toaster';
1514
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
1615
import { MultipartFileUploader } from '@gitroom/frontend/components/media/new.uploader';
1716
import dynamic from 'next/dynamic';
@@ -186,6 +185,7 @@ export const MediaBox: FC<{
186185
<img
187186
className="w-full h-full object-cover"
188187
src={mediaDirectory.set(media.path)}
188+
alt='media'
189189
/>
190190
)}
191191
</div>

apps/frontend/src/components/media/new.uploader.tsx

+17-54
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,14 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
22
// @ts-ignore
33
import Uppy, { UploadResult } from '@uppy/core';
44
// @ts-ignore
5-
import AwsS3Multipart from '@uppy/aws-s3-multipart';
6-
// @ts-ignore
75
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
8-
9-
import sha256 from 'sha256';
6+
import { getUppyUploadPlugin } from '@gitroom/react/helpers/uppy.upload';
107
import { FileInput, ProgressBar } from '@uppy/react';
118

129
// Uppy styles
1310
import '@uppy/core/dist/style.min.css';
1411
import '@uppy/dashboard/dist/style.min.css';
1512

16-
const fetchUploadApiEndpoint = async (
17-
fetch: any,
18-
endpoint: string,
19-
data: any
20-
) => {
21-
const res = await fetch(`/media/${endpoint}`, {
22-
method: 'POST',
23-
body: JSON.stringify(data),
24-
headers: {
25-
accept: 'application/json',
26-
'Content-Type': 'application/json',
27-
},
28-
});
29-
30-
return res.json();
31-
};
32-
3313
export function MultipartFileUploader({
3414
onUploadSuccess,
3515
allowedFileTypes,
@@ -41,7 +21,7 @@ export function MultipartFileUploader({
4121
const [loaded, setLoaded] = useState(false);
4222
const [reload, setReload] = useState(false);
4323

44-
const onUploadSuccessExtended = useCallback((result: UploadResult) => {
24+
const onUploadSuccessExtended = useCallback((result: UploadResult<any,any>) => {
4525
setReload(true);
4626
onUploadSuccess(result);
4727
}, [onUploadSuccess]);
@@ -78,7 +58,9 @@ export function MultipartFileUploaderAfter({
7858
onUploadSuccess: (result: UploadResult) => void;
7959
allowedFileTypes: string;
8060
}) {
61+
const storageProvider = process.env.NEXT_PUBLIC_STORAGE_PROVIDER || "local";
8162
const fetch = useFetch();
63+
8264
const uppy = useMemo(() => {
8365
const uppy2 = new Uppy({
8466
autoProceed: true,
@@ -87,38 +69,17 @@ export function MultipartFileUploaderAfter({
8769
allowedFileTypes: allowedFileTypes.split(','),
8870
maxFileSize: 1000000000,
8971
},
90-
}).use(AwsS3Multipart, {
91-
// @ts-ignore
92-
createMultipartUpload: async (file) => {
93-
const arrayBuffer = await new Response(file.data).arrayBuffer();
94-
// @ts-ignore
95-
const fileHash = await sha256(arrayBuffer);
96-
const contentType = file.type;
97-
return fetchUploadApiEndpoint(fetch, 'create-multipart-upload', {
98-
file,
99-
fileHash,
100-
contentType,
101-
});
102-
},
103-
// @ts-ignore
104-
listParts: (file, props) =>
105-
fetchUploadApiEndpoint(fetch, 'list-parts', { file, ...props }),
106-
// @ts-ignore
107-
signPart: (file, props) =>
108-
fetchUploadApiEndpoint(fetch, 'sign-part', { file, ...props }),
109-
// @ts-ignore
110-
abortMultipartUpload: (file, props) =>
111-
fetchUploadApiEndpoint(fetch, 'abort-multipart-upload', {
112-
file,
113-
...props,
114-
}),
115-
// @ts-ignore
116-
completeMultipartUpload: (file, props) =>
117-
fetchUploadApiEndpoint(fetch, 'complete-multipart-upload', {
118-
file,
119-
...props,
120-
}),
12172
});
73+
74+
const { plugin, options } = getUppyUploadPlugin(storageProvider, fetch)
75+
uppy2.use(plugin, options)
76+
// Set additional metadata when a file is added
77+
uppy2.on('file-added', (file) => {
78+
uppy2.setFileMeta(file.id, {
79+
useCloudflare: storageProvider === 'cloudflare' ? 'true' : 'false', // Example of adding a custom field
80+
// Add more fields as needed
81+
});
82+
});
12283

12384
uppy2.on('complete', (result) => {
12485
onUploadSuccess(result);
@@ -141,15 +102,17 @@ export function MultipartFileUploaderAfter({
141102

142103
return (
143104
<>
105+
{/* <Dashboard uppy={uppy} /> */}
144106
<ProgressBar uppy={uppy} />
145107
<FileInput
146108
uppy={uppy}
147109
locale={{
148110
strings: {
149111
chooseFiles: 'Upload',
150112
},
113+
pluralize: (n) => n
151114
}}
152-
/>
115+
/>
153116
</>
154117
);
155118
}

apps/frontend/src/middleware.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.man
77
export async function middleware(request: NextRequest) {
88
const nextUrl = request.nextUrl;
99
const authCookie = request.cookies.get('auth');
10-
10+
if (nextUrl.pathname.startsWith('/uploads/')) {
11+
return NextResponse.next();
12+
}
1113
// If the URL is logout, delete the cookie and redirect to login
1214
if (nextUrl.href.indexOf('/auth/logout') > -1) {
1315
const response = NextResponse.redirect(

libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts

+5-11
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import { Injectable } from '@nestjs/common';
33
import dayjs from 'dayjs';
44
import { Integration } from '@prisma/client';
55
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
6-
import { simpleUpload } from '@gitroom/nestjs-libraries/upload/r2.uploader';
7-
import axios from 'axios';
86
import { IntegrationTimeDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.time.dto';
7+
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
98

109
@Injectable()
1110
export class IntegrationRepository {
11+
private storage = UploadFactory.createStorage();
1212
constructor(
1313
private _integration: PrismaRepository<'integration'>,
1414
private _posts: PrismaRepository<'post'>
@@ -32,16 +32,10 @@ export class IntegrationRepository {
3232
async updateIntegration(id: string, params: Partial<Integration>) {
3333
if (
3434
params.picture &&
35-
params.picture.indexOf(process.env.CLOUDFLARE_BUCKET_URL!) === -1
35+
(params.picture.indexOf(process.env.CLOUDFLARE_BUCKET_URL!) === -1 ||
36+
params.picture.indexOf(process.env.FRONTEND_URL!) === -1)
3637
) {
37-
const picture = await axios.get(params.picture, {
38-
responseType: 'arraybuffer',
39-
});
40-
params.picture = await simpleUpload(
41-
picture.data,
42-
`${makeId(10)}.png`,
43-
'image/png'
44-
);
38+
params.picture = await this.storage.uploadSimple(params.picture);
4539
}
4640

4741
return this._integration.model.integration.update({

libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts

+3-7
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ import { timer } from '@gitroom/helpers/utils/timer';
2121
import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
2222
import { RefreshToken } from '@gitroom/nestjs-libraries/integrations/social.abstract';
2323
import { IntegrationTimeDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.time.dto';
24+
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
2425

2526
@Injectable()
2627
export class IntegrationService {
28+
private storage = UploadFactory.createStorage();
2729
constructor(
2830
private _integrationRepository: IntegrationRepository,
2931
private _integrationManager: IntegrationManager,
@@ -50,13 +52,7 @@ export class IntegrationService {
5052
timezone?: number,
5153
customInstanceDetails?: string
5254
) {
53-
const loadImage = await axios.get(picture, { responseType: 'arraybuffer' });
54-
const uploadedPicture = await simpleUpload(
55-
loadImage.data,
56-
`${makeId(10)}.png`,
57-
'image/png'
58-
);
59-
55+
const uploadedPicture = await this.storage.uploadSimple(picture);
6056
return this._integrationRepository.createOrUpdateIntegration(
6157
org,
6258
name,

0 commit comments

Comments
 (0)