commit
91823079c4
|
|
@ -29,11 +29,14 @@ CLOUDFLARE_REGION="auto"
|
|||
#EMAIL_FROM_ADDRESS=""
|
||||
#EMAIL_FROM_NAME=""
|
||||
|
||||
# Your upload directory path if you host your files locally, otherwise Cloudflare will be used.
|
||||
#UPLOAD_DIRECTORY="/opt/postiz/uploads/"
|
||||
# Where will social media icons be saved - local or cloudflare.
|
||||
STORAGE_PROVIDER="local"
|
||||
|
||||
# Your upload directory path if you host your files locally, otherwise Cloudflare will be used.
|
||||
#NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY="/opt/postiz/uploads/"
|
||||
#UPLOAD_DIRECTORY=""
|
||||
|
||||
# Your upload directory path if you host your files locally, otherwise Cloudflare will be used.
|
||||
#NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY=""
|
||||
|
||||
|
||||
# Social Media API Settings
|
||||
|
|
|
|||
|
|
@ -46,18 +46,6 @@ const authenticatedController = [
|
|||
@Module({
|
||||
imports: [
|
||||
UploadModule,
|
||||
...(!!process.env.UPLOAD_DIRECTORY &&
|
||||
!!process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY
|
||||
? [
|
||||
ServeStaticModule.forRoot({
|
||||
rootPath: process.env.UPLOAD_DIRECTORY,
|
||||
serveRoot: '/' + process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY,
|
||||
serveStaticOptions: {
|
||||
index: false,
|
||||
},
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
controllers: [
|
||||
RootController,
|
||||
|
|
|
|||
|
|
@ -10,10 +10,13 @@ import handleR2Upload from '@gitroom/nestjs-libraries/upload/r2.uploader';
|
|||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { CustomFileValidationPipe } from '@gitroom/nestjs-libraries/upload/custom.upload.validation';
|
||||
import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service';
|
||||
import { basename } from 'path';
|
||||
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
|
||||
|
||||
@ApiTags('Media')
|
||||
@Controller('/media')
|
||||
export class MediaController {
|
||||
private storage = UploadFactory.createStorage();
|
||||
constructor(
|
||||
private _mediaService: MediaService,
|
||||
private _subscriptionService: SubscriptionService
|
||||
|
|
@ -33,19 +36,26 @@ export class MediaController {
|
|||
return {output: 'data:image/png;base64,' + await this._mediaService.generateImage(prompt, org)};
|
||||
}
|
||||
|
||||
@Post('/upload-simple')
|
||||
@Post('/upload-server')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
@UsePipes(new CustomFileValidationPipe())
|
||||
async uploadServer(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@UploadedFile() file: Express.Multer.File
|
||||
) {
|
||||
const uploadedFile = await this.storage.uploadFile(file);
|
||||
const filePath = uploadedFile.path.replace(process.env.UPLOAD_DIRECTORY, basename(process.env.UPLOAD_DIRECTORY));
|
||||
return this._mediaService.saveFile(org.id, uploadedFile.originalname, filePath);
|
||||
}
|
||||
|
||||
@Post('/upload-simple')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async uploadSimple(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@UploadedFile('file')
|
||||
file: Express.Multer.File
|
||||
@UploadedFile('file') file: Express.Multer.File
|
||||
) {
|
||||
const filePath =
|
||||
file.path.indexOf('http') === 0
|
||||
? file.path
|
||||
: file.path.replace(process.env.UPLOAD_DIRECTORY, '');
|
||||
return this._mediaService.saveFile(org.id, file.originalname, filePath);
|
||||
const getFile = await this.storage.uploadFile(file);
|
||||
return this._mediaService.saveFile(org.id, getFile.originalname, getFile.path);
|
||||
}
|
||||
|
||||
@Post('/:endpoint')
|
||||
|
|
|
|||
|
|
@ -15,12 +15,37 @@ const nextConfig = {
|
|||
transpilePackages: ['crypto-hash'],
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'http',
|
||||
hostname: '**',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: '**',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: '/api/uploads/:path*',
|
||||
destination:
|
||||
process.env.STORAGE_PROVIDER === 'local' ? '/uploads/:path*' : '/404',
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/uploads/:path*',
|
||||
destination:
|
||||
process.env.STORAGE_PROVIDER === 'local'
|
||||
? '/api/uploads/:path*'
|
||||
: '/404',
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
const plugins = [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createReadStream, statSync } from 'fs';
|
||||
// @ts-ignore
|
||||
import mime from 'mime';
|
||||
|
||||
async function* nodeStreamToIterator(stream: any) {
|
||||
for await (const chunk of stream) {
|
||||
yield chunk;
|
||||
}
|
||||
}
|
||||
|
||||
function iteratorToStream(iterator: any) {
|
||||
return new ReadableStream({
|
||||
async pull(controller) {
|
||||
const { value, done } = await iterator.next();
|
||||
|
||||
if (done) {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.enqueue(new Uint8Array(value));
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const GET = (
|
||||
request: NextRequest,
|
||||
context: { params: { path: string[] } }
|
||||
) => {
|
||||
const filePath =
|
||||
process.env.UPLOAD_DIRECTORY + '/' + context.params.path.join('/');
|
||||
const response = createReadStream(filePath);
|
||||
|
||||
const fileStats = statSync(filePath);
|
||||
const contentType = mime.getType(filePath) || 'application/octet-stream';
|
||||
|
||||
const iterator = nodeStreamToIterator(response);
|
||||
const webStream = iteratorToStream(iterator);
|
||||
return new Response(webStream, {
|
||||
headers: {
|
||||
'Content-Type': contentType, // Set the appropriate content-type header
|
||||
'Content-Length': fileStats.size.toString(), // Set the content-length header
|
||||
'Last-Modified': fileStats.mtime.toUTCString(), // Set the last-modified header
|
||||
'Cache-Control': 'public, max-age=31536000, immutable', // Example cache-control header
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { ChangeEvent, FC, useCallback, useEffect, useState } from 'react';
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import useSWR from 'swr';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
|
|
@ -11,7 +11,6 @@ import EventEmitter from 'events';
|
|||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import clsx from 'clsx';
|
||||
import { VideoFrame } from '@gitroom/react/helpers/video.frame';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
|
||||
import { MultipartFileUploader } from '@gitroom/frontend/components/media/new.uploader';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
|
@ -186,6 +185,7 @@ export const MediaBox: FC<{
|
|||
<img
|
||||
className="w-full h-full object-cover"
|
||||
src={mediaDirectory.set(media.path)}
|
||||
alt='media'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,34 +2,14 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|||
// @ts-ignore
|
||||
import Uppy, { UploadResult } from '@uppy/core';
|
||||
// @ts-ignore
|
||||
import AwsS3Multipart from '@uppy/aws-s3-multipart';
|
||||
// @ts-ignore
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
|
||||
import sha256 from 'sha256';
|
||||
import { getUppyUploadPlugin } from '@gitroom/react/helpers/uppy.upload';
|
||||
import { FileInput, ProgressBar } from '@uppy/react';
|
||||
|
||||
// Uppy styles
|
||||
import '@uppy/core/dist/style.min.css';
|
||||
import '@uppy/dashboard/dist/style.min.css';
|
||||
|
||||
const fetchUploadApiEndpoint = async (
|
||||
fetch: any,
|
||||
endpoint: string,
|
||||
data: any
|
||||
) => {
|
||||
const res = await fetch(`/media/${endpoint}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
return res.json();
|
||||
};
|
||||
|
||||
export function MultipartFileUploader({
|
||||
onUploadSuccess,
|
||||
allowedFileTypes,
|
||||
|
|
@ -41,7 +21,7 @@ export function MultipartFileUploader({
|
|||
const [loaded, setLoaded] = useState(false);
|
||||
const [reload, setReload] = useState(false);
|
||||
|
||||
const onUploadSuccessExtended = useCallback((result: UploadResult) => {
|
||||
const onUploadSuccessExtended = useCallback((result: UploadResult<any,any>) => {
|
||||
setReload(true);
|
||||
onUploadSuccess(result);
|
||||
}, [onUploadSuccess]);
|
||||
|
|
@ -78,7 +58,9 @@ export function MultipartFileUploaderAfter({
|
|||
onUploadSuccess: (result: UploadResult) => void;
|
||||
allowedFileTypes: string;
|
||||
}) {
|
||||
const storageProvider = process.env.NEXT_PUBLIC_STORAGE_PROVIDER || "local";
|
||||
const fetch = useFetch();
|
||||
|
||||
const uppy = useMemo(() => {
|
||||
const uppy2 = new Uppy({
|
||||
autoProceed: true,
|
||||
|
|
@ -87,38 +69,17 @@ export function MultipartFileUploaderAfter({
|
|||
allowedFileTypes: allowedFileTypes.split(','),
|
||||
maxFileSize: 1000000000,
|
||||
},
|
||||
}).use(AwsS3Multipart, {
|
||||
// @ts-ignore
|
||||
createMultipartUpload: async (file) => {
|
||||
const arrayBuffer = await new Response(file.data).arrayBuffer();
|
||||
// @ts-ignore
|
||||
const fileHash = await sha256(arrayBuffer);
|
||||
const contentType = file.type;
|
||||
return fetchUploadApiEndpoint(fetch, 'create-multipart-upload', {
|
||||
file,
|
||||
fileHash,
|
||||
contentType,
|
||||
});
|
||||
},
|
||||
// @ts-ignore
|
||||
listParts: (file, props) =>
|
||||
fetchUploadApiEndpoint(fetch, 'list-parts', { file, ...props }),
|
||||
// @ts-ignore
|
||||
signPart: (file, props) =>
|
||||
fetchUploadApiEndpoint(fetch, 'sign-part', { file, ...props }),
|
||||
// @ts-ignore
|
||||
abortMultipartUpload: (file, props) =>
|
||||
fetchUploadApiEndpoint(fetch, 'abort-multipart-upload', {
|
||||
file,
|
||||
...props,
|
||||
}),
|
||||
// @ts-ignore
|
||||
completeMultipartUpload: (file, props) =>
|
||||
fetchUploadApiEndpoint(fetch, 'complete-multipart-upload', {
|
||||
file,
|
||||
...props,
|
||||
}),
|
||||
});
|
||||
|
||||
const { plugin, options } = getUppyUploadPlugin(storageProvider, fetch)
|
||||
uppy2.use(plugin, options)
|
||||
// Set additional metadata when a file is added
|
||||
uppy2.on('file-added', (file) => {
|
||||
uppy2.setFileMeta(file.id, {
|
||||
useCloudflare: storageProvider === 'cloudflare' ? 'true' : 'false', // Example of adding a custom field
|
||||
// Add more fields as needed
|
||||
});
|
||||
});
|
||||
|
||||
uppy2.on('complete', (result) => {
|
||||
onUploadSuccess(result);
|
||||
|
|
@ -141,6 +102,7 @@ export function MultipartFileUploaderAfter({
|
|||
|
||||
return (
|
||||
<>
|
||||
{/* <Dashboard uppy={uppy} /> */}
|
||||
<ProgressBar uppy={uppy} />
|
||||
<FileInput
|
||||
uppy={uppy}
|
||||
|
|
@ -148,8 +110,9 @@ export function MultipartFileUploaderAfter({
|
|||
strings: {
|
||||
chooseFiles: 'Upload',
|
||||
},
|
||||
pluralize: (n) => n
|
||||
}}
|
||||
/>
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.man
|
|||
export async function middleware(request: NextRequest) {
|
||||
const nextUrl = request.nextUrl;
|
||||
const authCookie = request.cookies.get('auth');
|
||||
|
||||
if (nextUrl.pathname.startsWith('/uploads/')) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
// If the URL is logout, delete the cookie and redirect to login
|
||||
if (nextUrl.href.indexOf('/auth/logout') > -1) {
|
||||
const response = NextResponse.redirect(
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ import { Injectable } from '@nestjs/common';
|
|||
import dayjs from 'dayjs';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { simpleUpload } from '@gitroom/nestjs-libraries/upload/r2.uploader';
|
||||
import axios from 'axios';
|
||||
import { IntegrationTimeDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.time.dto';
|
||||
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
|
||||
|
||||
@Injectable()
|
||||
export class IntegrationRepository {
|
||||
private storage = UploadFactory.createStorage();
|
||||
constructor(
|
||||
private _integration: PrismaRepository<'integration'>,
|
||||
private _posts: PrismaRepository<'post'>
|
||||
|
|
@ -32,16 +32,10 @@ export class IntegrationRepository {
|
|||
async updateIntegration(id: string, params: Partial<Integration>) {
|
||||
if (
|
||||
params.picture &&
|
||||
params.picture.indexOf(process.env.CLOUDFLARE_BUCKET_URL!) === -1
|
||||
(params.picture.indexOf(process.env.CLOUDFLARE_BUCKET_URL!) === -1 ||
|
||||
params.picture.indexOf(process.env.FRONTEND_URL!) === -1)
|
||||
) {
|
||||
const picture = await axios.get(params.picture, {
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
params.picture = await simpleUpload(
|
||||
picture.data,
|
||||
`${makeId(10)}.png`,
|
||||
'image/png'
|
||||
);
|
||||
params.picture = await this.storage.uploadSimple(params.picture);
|
||||
}
|
||||
|
||||
return this._integration.model.integration.update({
|
||||
|
|
|
|||
|
|
@ -21,9 +21,11 @@ import { timer } from '@gitroom/helpers/utils/timer';
|
|||
import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
|
||||
import { RefreshToken } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { IntegrationTimeDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.time.dto';
|
||||
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
|
||||
|
||||
@Injectable()
|
||||
export class IntegrationService {
|
||||
private storage = UploadFactory.createStorage();
|
||||
constructor(
|
||||
private _integrationRepository: IntegrationRepository,
|
||||
private _integrationManager: IntegrationManager,
|
||||
|
|
@ -50,13 +52,7 @@ export class IntegrationService {
|
|||
timezone?: number,
|
||||
customInstanceDetails?: string
|
||||
) {
|
||||
const loadImage = await axios.get(picture, { responseType: 'arraybuffer' });
|
||||
const uploadedPicture = await simpleUpload(
|
||||
loadImage.data,
|
||||
`${makeId(10)}.png`,
|
||||
'image/png'
|
||||
);
|
||||
|
||||
const uploadedPicture = await this.storage.uploadSimple(picture);
|
||||
return this._integrationRepository.createOrUpdateIntegration(
|
||||
org,
|
||||
name,
|
||||
|
|
|
|||
|
|
@ -120,6 +120,8 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
|||
for (const post of postDetails) {
|
||||
const images = await Promise.all(
|
||||
post.media?.map(async (p) => {
|
||||
const a = await fetch(p.url);
|
||||
console.log(p.url);
|
||||
return await agent.uploadBlob(
|
||||
new Blob([
|
||||
await sharp(await (await fetch(p.url)).arrayBuffer())
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider {
|
|||
|
||||
let finalId = '';
|
||||
let finalUrl = '';
|
||||
if ((firstPost?.media?.[0]?.path?.indexOf('mp4') || -2) > -1) {
|
||||
if ((firstPost?.media?.[0]?.url?.indexOf('mp4') || -2) > -1) {
|
||||
const { id: videoId, permalink_url, ...all } = await (
|
||||
await this.fetch(
|
||||
`https://graph.facebook.com/v20.0/${id}/videos?access_token=${accessToken}&fields=id,permalink_url`,
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ export class InstagramProvider
|
|||
const isCarousel =
|
||||
(firstPost?.media?.length || 0) > 1 ? `&is_carousel_item=true` : ``;
|
||||
const mediaType =
|
||||
m.path.indexOf('.mp4') > -1
|
||||
m.url.indexOf('.mp4') > -1
|
||||
? firstPost?.media?.length === 1
|
||||
? `video_url=${m.url}&media_type=REELS`
|
||||
: `video_url=${m.url}&media_type=VIDEO`
|
||||
|
|
|
|||
|
|
@ -299,13 +299,13 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
|||
p?.media?.flatMap(async (m) => {
|
||||
return {
|
||||
id: await this.uploadPicture(
|
||||
m.path,
|
||||
m.url,
|
||||
accessToken,
|
||||
id,
|
||||
m.path.indexOf('mp4') > -1
|
||||
? Buffer.from(await readOrFetch(m.path))
|
||||
: await sharp(await readOrFetch(m.path), {
|
||||
animated: lookup(m.path) === 'image/gif',
|
||||
m.url.indexOf('mp4') > -1
|
||||
? Buffer.from(await readOrFetch(m.url))
|
||||
: await sharp(await readOrFetch(m.url), {
|
||||
animated: lookup(m.url) === 'image/gif',
|
||||
})
|
||||
.resize({
|
||||
width: 1000,
|
||||
|
|
|
|||
|
|
@ -153,10 +153,10 @@ export class PinterestProvider
|
|||
): Promise<PostResponse[]> {
|
||||
let mediaId = '';
|
||||
const findMp4 = postDetails?.[0]?.media?.find(
|
||||
(p) => (p.path?.indexOf('mp4') || -1) > -1
|
||||
(p) => (p.url?.indexOf('mp4') || -1) > -1
|
||||
);
|
||||
const picture = postDetails?.[0]?.media?.find(
|
||||
(p) => (p.path?.indexOf('mp4') || -1) === -1
|
||||
(p) => (p.url?.indexOf('mp4') || -1) === -1
|
||||
);
|
||||
|
||||
if (findMp4) {
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
|
|||
? {
|
||||
url: `${
|
||||
firstPostSettings.value.media[0].path.indexOf('http') === -1
|
||||
? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY}`
|
||||
? `${process.env.NEXT_PUBLIC_BACKEND_URL}/uploads`
|
||||
: ``
|
||||
}${firstPostSettings.value.media[0].path}`,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,18 +154,18 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
|||
let link = '';
|
||||
|
||||
if (firstPost?.media?.length! <= 1) {
|
||||
const type = !firstPost?.media?.[0]?.path
|
||||
const type = !firstPost?.media?.[0]?.url
|
||||
? undefined
|
||||
: firstPost?.media![0].path.indexOf('.mp4') > -1
|
||||
: firstPost?.media![0].url.indexOf('.mp4') > -1
|
||||
? 'video_url'
|
||||
: 'image_url';
|
||||
|
||||
const media = new URLSearchParams({
|
||||
...(type === 'video_url'
|
||||
? { video_url: firstPost?.media![0].path }
|
||||
? { video_url: firstPost?.media![0].url }
|
||||
: {}),
|
||||
...(type === 'image_url'
|
||||
? { image_url: firstPost?.media![0].path }
|
||||
? { image_url: firstPost?.media![0].url }
|
||||
: {}),
|
||||
media_type:
|
||||
type === 'video_url'
|
||||
|
|
@ -209,11 +209,11 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
|
|||
const medias = [];
|
||||
for (const mediaLoad of firstPost.media!) {
|
||||
const type =
|
||||
mediaLoad.path.indexOf('.mp4') > -1 ? 'video_url' : 'image_url';
|
||||
mediaLoad.url.indexOf('.mp4') > -1 ? 'video_url' : 'image_url';
|
||||
|
||||
const media = new URLSearchParams({
|
||||
...(type === 'video_url' ? { video_url: mediaLoad.path } : {}),
|
||||
...(type === 'image_url' ? { image_url: mediaLoad.path } : {}),
|
||||
...(type === 'video_url' ? { video_url: mediaLoad.url } : {}),
|
||||
...(type === 'image_url' ? { image_url: mediaLoad.url } : {}),
|
||||
is_carousel_item: 'true',
|
||||
media_type:
|
||||
type === 'video_url'
|
||||
|
|
|
|||
|
|
@ -132,10 +132,10 @@ export class XProvider extends SocialAbstract implements SocialProvider {
|
|||
p?.media?.flatMap(async (m) => {
|
||||
return {
|
||||
id: await client.v1.uploadMedia(
|
||||
m.path.indexOf('mp4') > -1
|
||||
? Buffer.from(await readOrFetch(m.path))
|
||||
: await sharp(await readOrFetch(m.path), {
|
||||
animated: lookup(m.path) === 'image/gif',
|
||||
m.url.indexOf('mp4') > -1
|
||||
? Buffer.from(await readOrFetch(m.url))
|
||||
: await sharp(await readOrFetch(m.url), {
|
||||
animated: lookup(m.url) === 'image/gif',
|
||||
})
|
||||
.resize({
|
||||
width: 1000,
|
||||
|
|
@ -143,7 +143,7 @@ export class XProvider extends SocialAbstract implements SocialProvider {
|
|||
.gif()
|
||||
.toBuffer(),
|
||||
{
|
||||
mimeType: lookup(m.path) || '',
|
||||
mimeType: lookup(m.url) || '',
|
||||
}
|
||||
),
|
||||
postId: p.id,
|
||||
|
|
|
|||
|
|
@ -158,11 +158,11 @@ export class YoutubeProvider extends SocialAbstract implements SocialProvider {
|
|||
...(settings?.tags?.length
|
||||
? { tags: settings.tags.map((p) => p.label) }
|
||||
: {}),
|
||||
// ...(settings?.thumbnail?.path
|
||||
// ...(settings?.thumbnail?.url
|
||||
// ? {
|
||||
// thumbnails: {
|
||||
// default: {
|
||||
// url: settings?.thumbnail?.path,
|
||||
// url: settings?.thumbnail?.url,
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,25 +1,22 @@
|
|||
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
||||
import concat from 'concat-stream';
|
||||
import { StorageEngine } from 'multer';
|
||||
import type { Request } from 'express';
|
||||
import {makeId} from "@gitroom/nestjs-libraries/services/make.is";
|
||||
import 'multer';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import mime from 'mime-types';
|
||||
// @ts-ignore
|
||||
import {getExtension} from 'mime';
|
||||
import { IUploadProvider } from './upload.interface';
|
||||
import axios from 'axios';
|
||||
|
||||
type CallbackFunction = (
|
||||
error: Error | null,
|
||||
info?: Partial<Express.Multer.File>
|
||||
) => void;
|
||||
|
||||
class CloudflareStorage implements StorageEngine {
|
||||
class CloudflareStorage implements IUploadProvider {
|
||||
private _client: S3Client;
|
||||
|
||||
public constructor(
|
||||
constructor(
|
||||
accountID: string,
|
||||
accessKey: string,
|
||||
secretKey: string,
|
||||
private region: string,
|
||||
private _bucketName: string,
|
||||
private _uploadUrl: string,
|
||||
private _uploadUrl: string
|
||||
) {
|
||||
this._client = new S3Client({
|
||||
endpoint: `https://${accountID}.r2.cloudflarestorage.com`,
|
||||
|
|
@ -31,56 +28,61 @@ class CloudflareStorage implements StorageEngine {
|
|||
});
|
||||
}
|
||||
|
||||
public _handleFile(
|
||||
_req: Request,
|
||||
file: Express.Multer.File,
|
||||
callback: CallbackFunction
|
||||
): void {
|
||||
file.stream.pipe(
|
||||
concat({ encoding: 'buffer' }, async (data) => {
|
||||
// @ts-ignore
|
||||
callback(null, await this._uploadFile(data, data.length, file.mimetype, mime.extension(file.mimetype)));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public _removeFile(
|
||||
_req: Request,
|
||||
file: Express.Multer.File,
|
||||
callback: (error: Error | null) => void
|
||||
): void {
|
||||
void this._deleteFile(file.destination, callback);
|
||||
}
|
||||
|
||||
private async _uploadFile(data: Buffer, size: number, mime: string, extension: string): Promise<Express.Multer.File> {
|
||||
async uploadSimple(path: string) {
|
||||
const loadImage = await axios.get(path, { responseType: 'arraybuffer' });
|
||||
const contentType = loadImage?.headers?.['content-type'] || loadImage?.headers?.['Content-Type'];
|
||||
const extension = getExtension(contentType)!;
|
||||
const id = makeId(10);
|
||||
|
||||
const params = {
|
||||
Bucket: this._bucketName,
|
||||
Key: `${id}.${extension}`,
|
||||
Body: loadImage.data,
|
||||
ContentType: contentType,
|
||||
};
|
||||
|
||||
const command = new PutObjectCommand({ ...params });
|
||||
await this._client.send(command);
|
||||
|
||||
return `${this._uploadUrl}/${id}.${extension}`;
|
||||
}
|
||||
|
||||
async uploadFile(file: Express.Multer.File): Promise<any> {
|
||||
const id = makeId(10);
|
||||
const extension = mime.extension(file.mimetype) || '';
|
||||
|
||||
// Create the PutObjectCommand to upload the file to Cloudflare R2
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: this._bucketName,
|
||||
ACL: 'public-read',
|
||||
Key: `${id}.${extension}`,
|
||||
Body: data,
|
||||
Body: file.buffer,
|
||||
});
|
||||
|
||||
await this._client.send(command);
|
||||
|
||||
return {
|
||||
filename: `${id}.${extension}`,
|
||||
mimetype: mime,
|
||||
size,
|
||||
buffer: data,
|
||||
mimetype: file.mimetype,
|
||||
size: file.size,
|
||||
buffer: file.buffer,
|
||||
originalname: `${id}.${extension}`,
|
||||
fieldname: 'file',
|
||||
path: `${this._uploadUrl}/${id}.${extension}`,
|
||||
destination: `${this._uploadUrl}/${id}.${extension}`,
|
||||
encoding: '7bit',
|
||||
stream: data as any,
|
||||
}
|
||||
stream: file.buffer as any,
|
||||
};
|
||||
}
|
||||
|
||||
private async _deleteFile(
|
||||
filedestination: string,
|
||||
callback: CallbackFunction
|
||||
) {
|
||||
// Implement the removeFile method from IUploadProvider
|
||||
async removeFile(filePath: string): Promise<void> {
|
||||
// const fileName = filePath.split('/').pop(); // Extract the filename from the path
|
||||
// const command = new DeleteObjectCommand({
|
||||
// Bucket: this._bucketName,
|
||||
// Key: fileName,
|
||||
// });
|
||||
// await this._client.send(command);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
import { IUploadProvider } from './upload.interface';
|
||||
import { mkdirSync, unlink, writeFileSync } from 'fs';
|
||||
// @ts-ignore
|
||||
import mime from 'mime';
|
||||
import { extname } from 'path';
|
||||
import axios from 'axios';
|
||||
|
||||
export class LocalStorage implements IUploadProvider {
|
||||
constructor(private uploadDirectory: string) {}
|
||||
|
||||
async uploadSimple(path: string) {
|
||||
const loadImage = await axios.get(path, { responseType: 'arraybuffer' });
|
||||
const contentType = loadImage?.headers?.['content-type'] || loadImage?.headers?.['Content-Type'];
|
||||
const findExtension = mime.getExtension(contentType)!;
|
||||
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
|
||||
const innerPath = `/${year}/${month}/${day}`;
|
||||
const dir = `${this.uploadDirectory}${innerPath}`;
|
||||
mkdirSync(dir, { recursive: true });
|
||||
|
||||
const randomName = Array(32)
|
||||
.fill(null)
|
||||
.map(() => Math.round(Math.random() * 16).toString(16))
|
||||
.join('');
|
||||
|
||||
const filePath = `${dir}/${randomName}.${findExtension}`;
|
||||
const publicPath = `${innerPath}/${randomName}.${findExtension}`;
|
||||
// Logic to save the file to the filesystem goes here
|
||||
writeFileSync(filePath, loadImage.data);
|
||||
|
||||
return process.env.FRONTEND_URL + '/uploads' + publicPath;
|
||||
}
|
||||
|
||||
async uploadFile(file: Express.Multer.File): Promise<any> {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
|
||||
const innerPath = `/${year}/${month}/${day}`;
|
||||
const dir = `${this.uploadDirectory}${innerPath}`;
|
||||
mkdirSync(dir, { recursive: true });
|
||||
|
||||
const randomName = Array(32)
|
||||
.fill(null)
|
||||
.map(() => Math.round(Math.random() * 16).toString(16))
|
||||
.join('');
|
||||
|
||||
const filePath = `${dir}/${randomName}${extname(file.originalname)}`;
|
||||
const publicPath = `${innerPath}/${randomName}${extname(
|
||||
file.originalname
|
||||
)}`;
|
||||
console.log(filePath);
|
||||
// Logic to save the file to the filesystem goes here
|
||||
writeFileSync(filePath, file.buffer);
|
||||
|
||||
return {
|
||||
filename: `${randomName}${extname(file.originalname)}`,
|
||||
path: process.env.FRONTEND_URL + '/uploads' + publicPath,
|
||||
mimetype: file.mimetype,
|
||||
originalname: file.originalname,
|
||||
};
|
||||
}
|
||||
|
||||
async removeFile(filePath: string): Promise<void> {
|
||||
// Logic to remove the file from the filesystem goes here
|
||||
return new Promise((resolve, reject) => {
|
||||
unlink(filePath, (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { CloudflareStorage } from './cloudflare.storage';
|
||||
import { IUploadProvider } from './upload.interface';
|
||||
import { LocalStorage } from './local.storage';
|
||||
|
||||
export class UploadFactory {
|
||||
static createStorage(): IUploadProvider {
|
||||
const storageProvider = process.env.STORAGE_PROVIDER || 'local';
|
||||
|
||||
switch (storageProvider) {
|
||||
case 'local':
|
||||
return new LocalStorage(process.env.UPLOAD_DIRECTORY!);
|
||||
case 'cloudflare':
|
||||
return new CloudflareStorage(
|
||||
process.env.CLOUDFLARE_ACCOUNT_ID!,
|
||||
process.env.CLOUDFLARE_ACCESS_KEY!,
|
||||
process.env.CLOUDFLARE_SECRET_ACCESS_KEY!,
|
||||
process.env.CLOUDFLARE_REGION!,
|
||||
process.env.CLOUDFLARE_BUCKETNAME!,
|
||||
process.env.CLOUDFLARE_BUCKET_URL!
|
||||
);
|
||||
default:
|
||||
throw new Error(`Invalid storage type ${storageProvider}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
export interface IUploadProvider {
|
||||
uploadSimple(path: string): Promise<string>;
|
||||
uploadFile(file: Express.Multer.File): Promise<any>;
|
||||
removeFile(filePath: string): Promise<void>;
|
||||
}
|
||||
|
|
@ -1,60 +1,13 @@
|
|||
import { Global, Module } from '@nestjs/common';
|
||||
import { MulterModule } from '@nestjs/platform-express';
|
||||
import { diskStorage } from 'multer';
|
||||
import { mkdirSync } from 'fs';
|
||||
import { extname } from 'path';
|
||||
import CloudflareStorage from '@gitroom/nestjs-libraries/upload/cloudflare.storage';
|
||||
import { UploadFactory } from './upload.factory';
|
||||
import { CustomFileValidationPipe } from '@gitroom/nestjs-libraries/upload/custom.upload.validation';
|
||||
|
||||
const storage =
|
||||
process.env.CLOUDFLARE_ACCOUNT_ID &&
|
||||
process.env.CLOUDFLARE_ACCESS_KEY &&
|
||||
process.env.CLOUDFLARE_SECRET_ACCESS_KEY &&
|
||||
process.env.CLOUDFLARE_REGION &&
|
||||
process.env.CLOUDFLARE_BUCKETNAME &&
|
||||
process.env.CLOUDFLARE_BUCKET_URL
|
||||
? new CloudflareStorage(
|
||||
process.env.CLOUDFLARE_ACCOUNT_ID,
|
||||
process.env.CLOUDFLARE_ACCESS_KEY,
|
||||
process.env.CLOUDFLARE_SECRET_ACCESS_KEY,
|
||||
process.env.CLOUDFLARE_REGION,
|
||||
process.env.CLOUDFLARE_BUCKETNAME,
|
||||
process.env.CLOUDFLARE_BUCKET_URL
|
||||
)
|
||||
: diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0'); // Month is zero-based, hence +1
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
|
||||
const dir = `${process.env.UPLOAD_DIRECTORY}/${year}/${month}/${day}`;
|
||||
|
||||
// Create the directory if it doesn't exist
|
||||
mkdirSync(dir, { recursive: true });
|
||||
|
||||
cb(null, dir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
// Generate a unique filename here if needed
|
||||
const randomName = Array(32)
|
||||
.fill(null)
|
||||
.map(() => Math.round(Math.random() * 16).toString(16))
|
||||
.join('');
|
||||
cb(null, `${randomName}${extname(file.originalname)}`);
|
||||
},
|
||||
});
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
MulterModule.register({
|
||||
storage,
|
||||
}),
|
||||
],
|
||||
providers: [CustomFileValidationPipe],
|
||||
get exports() {
|
||||
return [...this.imports, ...this.providers];
|
||||
},
|
||||
providers: [UploadFactory, CustomFileValidationPipe],
|
||||
exports: [UploadFactory, CustomFileValidationPipe],
|
||||
})
|
||||
|
||||
export class UploadModule {}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
import XHRUpload from '@uppy/xhr-upload';
|
||||
import AwsS3Multipart from '@uppy/aws-s3';
|
||||
import sha256 from 'sha256';
|
||||
|
||||
const fetchUploadApiEndpoint = async (
|
||||
fetch: any,
|
||||
endpoint: string,
|
||||
data: any
|
||||
) => {
|
||||
const res = await fetch(`/media/${endpoint}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
return res.json();
|
||||
};
|
||||
|
||||
// Define the factory to return appropriate Uppy configuration
|
||||
export const getUppyUploadPlugin = (provider: string, fetch: any) => {
|
||||
switch (provider) {
|
||||
case 'cloudflare':
|
||||
return {
|
||||
plugin: AwsS3Multipart,
|
||||
options: {
|
||||
createMultipartUpload: async (file: any) => {
|
||||
const arrayBuffer = await new Response(file.data).arrayBuffer();
|
||||
const fileHash = sha256(Buffer.from(arrayBuffer));
|
||||
const contentType = file.type;
|
||||
return fetchUploadApiEndpoint(fetch, 'create-multipart-upload', {
|
||||
file,
|
||||
fileHash,
|
||||
contentType,
|
||||
});
|
||||
},
|
||||
listParts: (file: any, props: any) =>
|
||||
fetchUploadApiEndpoint(fetch, 'list-parts', { file, ...props }),
|
||||
signPart: (file: any, props: any) =>
|
||||
fetchUploadApiEndpoint(fetch, 'sign-part', { file, ...props }),
|
||||
abortMultipartUpload: (file: any, props: any) =>
|
||||
fetchUploadApiEndpoint(fetch, 'abort-multipart-upload', {
|
||||
file,
|
||||
...props,
|
||||
}),
|
||||
completeMultipartUpload: (file: any, props: any) =>
|
||||
fetchUploadApiEndpoint(fetch, 'complete-multipart-upload', {
|
||||
file,
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
case 'local':
|
||||
return {
|
||||
plugin: XHRUpload,
|
||||
options: {
|
||||
endpoint: `${process.env.NEXT_PUBLIC_BACKEND_URL}/media/upload-server`,
|
||||
withCredentials: true,
|
||||
},
|
||||
};
|
||||
|
||||
// Add more cases for other cloud providers
|
||||
default:
|
||||
throw new Error(`Unsupported storage provider: ${provider}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,8 @@
|
|||
import {useCallback} from "react";
|
||||
import { useVariables } from './variable.context';
|
||||
|
||||
export const useMediaDirectory = () => {
|
||||
const {backendUrl, uploadDirectory} = useVariables();
|
||||
const set = useCallback((path: string) => {
|
||||
if (path.indexOf('https') === 0) {
|
||||
return path;
|
||||
}
|
||||
return `${backendUrl}/${uploadDirectory}${path}`;
|
||||
return path;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
|
|
@ -69,6 +69,7 @@
|
|||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/md5": "^2.3.5",
|
||||
"@types/mime": "^4.0.0",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/multer": "^1.4.11",
|
||||
"@types/nodemailer": "^6.4.16",
|
||||
|
|
@ -78,13 +79,15 @@
|
|||
"@types/yup": "^0.32.0",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@uiw/react-md-editor": "^4.0.3",
|
||||
"@uppy/aws-s3-multipart": "^3.6.0",
|
||||
"@uppy/core": "^3.5.0",
|
||||
"@uppy/dashboard": "^3.5.2",
|
||||
"@uppy/drag-drop": "^3.0.3",
|
||||
"@uppy/file-input": "^3.0.3",
|
||||
"@uppy/progress-bar": "^3.0.3",
|
||||
"@uppy/react": "^3.1.3",
|
||||
"@uppy/aws-s3": "^4.1.0",
|
||||
"@uppy/core": "^4.2.0",
|
||||
"@uppy/dashboard": "^4.1.0",
|
||||
"@uppy/drag-drop": "^4.0.2",
|
||||
"@uppy/file-input": "^4.0.1",
|
||||
"@uppy/progress-bar": "^4.0.0",
|
||||
"@uppy/react": "^4.0.2",
|
||||
"@uppy/status-bar": "^4.0.3",
|
||||
"@uppy/xhr-upload": "^4.1.0",
|
||||
"array-move": "^4.0.0",
|
||||
"axios": "1.6.7",
|
||||
"bcrypt": "^5.1.1",
|
||||
|
|
@ -107,6 +110,7 @@
|
|||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"md5": "^2.3.0",
|
||||
"mime": "^3.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nestjs-command": "^3.1.4",
|
||||
|
|
|
|||
Loading…
Reference in New Issue