diff --git a/api/src/api/v1/Files.ts b/api/src/api/v1/Files.ts index d3d3aae8c..c193c2074 100644 --- a/api/src/api/v1/Files.ts +++ b/api/src/api/v1/Files.ts @@ -307,17 +307,19 @@ export class Files { const files = await prisma.files.findMany({ where: { AND: [ - { - name: { - startsWith: body.name.replace(/\.part0*\d+$/, '') - } - }, - { - user_id: req.user?.id - }, - { - parent_id: source.parent_id - }, + { name: source.name.endsWith('.part001') ? { startsWith: source.name.replace(/\.part0*\d+$/, '.part') } : source.name }, + { user_id: req.user?.id }, + { parent_id: source.parent_id }, + ] + } + }) + + const countExists = await prisma.files.count({ + where: { + AND: [ + { name: source.name.endsWith('.part001') ? { startsWith: source.name.replace(/\.part0*\d+$/, ''), endsWith: '.part001' } : { startsWith: source.name } }, + { user_id: req.user?.id }, + { parent_id: body.parent_id } ] } }) @@ -328,8 +330,8 @@ export class Files { const { forward_info: forwardInfo, message_id: messageId, mime_type: mimeType } = file let peerFrom: Api.InputPeerChannel | Api.InputPeerUser | Api.InputPeerChat let peerTo: Api.InputPeerChannel | Api.InputPeerUser | Api.InputPeerChat - const [type, peerId, _id, accessHash] = forwardInfo?.split('/') ?? [] if (forwardInfo && forwardInfo.match(/^channel\//gi)) { + const [type, peerId, _id, accessHash] = forwardInfo?.split('/') ?? [] if (type === 'channel') { peerFrom = new Api.InputPeerChannel({ channelId: bigInt(peerId), @@ -343,8 +345,8 @@ export class Files { chatId: bigInt(peerId) }) } } + const [type, peerId, _, accessHash] = ((req.user.settings as Prisma.JsonObject).saved_location as string).split('/') if ((req.user.settings as Prisma.JsonObject)?.saved_location) { - const [type, peerId, _, accessHash] = ((req.user.settings as Prisma.JsonObject).saved_location as string).split('/') if (type === 'channel') { peerTo = new Api.InputPeerChannel({ channelId: bigInt(peerId), @@ -368,7 +370,7 @@ export class Files { dropAuthor: true })) as any - const newForwardInfo = forwardInfo ? `${type}/${peerId}/${chat.updates[0].id.toString()}/${accessHash}` : null + const newForwardInfo = peerTo ? `${type}/${peerId}/${chat.updates[0].id.toString()}/${accessHash}` : null const message = { size: Number(file.size), message_id: chat.updates[0].id.toString(), @@ -380,7 +382,7 @@ export class Files { const response = await prisma.files.create({ data: { ...body, - name: files.length == 1 ? body.name : body.name.replace(/\.part0*\d+$/, '')+`.part${String(countFiles + 1).padStart(3, '0')}`, + name: files.length == 1 ? body.name + `${countExists ? ` (${countExists})` : ''}` : body.name.replace(/\.part0*\d+$/, '')+`${countExists ? ` (${countExists})` : ''}`+`.part${String(countFiles + 1).padStart(3, '0')}`, ...message } }) @@ -1065,15 +1067,16 @@ export class Files { const getSizes = ({ size, sizes }) => sizes ? sizes.pop() : size const size = file.media.photo ? getSizes(file.media.photo.sizes.pop()) : file.media.document?.size - let type = file.media.photo || mimeType.match(/^image/gi) ? 'image' : null + let type = file.media.photo if (file.media.document?.mimeType.match(/^video/gi) || name.match(/\.mp4$/gi) || name.match(/\.mkv$/gi) || name.match(/\.mov$/gi)) { type = 'video' } else if (file.media.document?.mimeType.match(/pdf$/gi) || name.match(/\.doc$/gi) || name.match(/\.docx$/gi) || name.match(/\.xls$/gi) || name.match(/\.xlsx$/gi)) { type = 'document' } else if (file.media.document?.mimeType.match(/audio$/gi) || name.match(/\.mp3$/gi) || name.match(/\.ogg$/gi)) { type = 'audio' + } else if (file.media.document?.mimeType.match(/^image/gi) || name.match(/\.jpg$/gi) || name.match(/\.jpeg$/gi) || name.match(/\.png$/gi) || name.match(/\.gif$/gi)) { + type = 'image' } - return { name, message_id: file.id.toString(), @@ -1200,7 +1203,7 @@ export class Files { const end = ranges[1] ? ranges[1] : totalFileSize.toJSNumber() - 1 const readStream = createReadStream(filename(), { start, end }) - res.writeHead(206, { + res.writeHead(200, { 'Cache-Control': 'public, max-age=604800', 'ETag': Buffer.from(`${files[0].id}:${files[0].message_id}`).toString('base64'), 'Content-Range': `bytes ${start}-${end}/${totalFileSize}`, @@ -1211,7 +1214,7 @@ export class Files { }) readStream.pipe(res) } else { - res.writeHead(206, { + res.writeHead(200, { 'Cache-Control': 'public, max-age=604800', 'ETag': Buffer.from(`${files[0].id}:${files[0].message_id}`).toString('base64'), 'Content-Range': `bytes */${totalFileSize}`, diff --git a/docs/docs/Installation/manual.md b/docs/docs/Installation/manual.md index 9a9061d7b..ad8d84896 100644 --- a/docs/docs/Installation/manual.md +++ b/docs/docs/Installation/manual.md @@ -51,7 +51,10 @@ If it's succeed you don't need to follow the steps below. npm i -g yarn ``` -- Define all api variables in `./api/.env`, you can copy from `./api/.env.example` + +- Define all +variables in `./api/.env`, you can copy from `./api/.env.example` + ```shell cp ./api/.env.example ./api/.env @@ -124,7 +127,10 @@ yarn workspaces run build ## Run: ```shell + yarn api prisma migrate deploy + + cd api && node dist/index.js ``` @@ -139,9 +145,8 @@ git pull origin main # or, staging for the latest updates yarn install # install yarn workspaces run build # build - -yarn api prisma migrate deploy +yarn api prisma migrate deploy cd api && node dist/index.js # run ``` -Next, you can deploy TeleDrive with [Vercel](/docs/deployment/vercel) or [PM2](/docs/deployment/pm2). \ No newline at end of file +Next, you can deploy TeleDrive with [Vercel](/docs/deployment/vercel) or [PM2](/docs/deployment/pm2). diff --git a/install.caprover.sh b/install.caprover.sh new file mode 100644 index 000000000..979ca29f9 --- /dev/null +++ b/install.caprover.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +set -e + +echo "Node Version: $(node -v)" +echo "Yarn Version: $(yarn -v)" + +if [ ! -f docker/.env ] +then +echo "Generating docker/.env file..." + +ENV="develop" + +echo "Preparing your keys from https://my.telegram.org/" +read -p "Enter your TG_API_ID: " TG_API_ID +read -p "Enter your TG_API_HASH: " TG_API_HASH + +echo "ENV=$ENV" > docker/.env +echo "TG_API_ID=$TG_API_ID" >> docker/.env +echo "TG_API_HASH=$TG_API_HASH" >> docker/.env +fi + +git reset --hard +git clean -f +git pull origin main + +export $(cat docker/.env | xargs) + +echo +echo "Build and deploy to CapRover..." +docker build --build-arg REACT_APP_TG_API_ID=$TG_API_ID --build-arg REACT_APP_TG_API_HASH=$TG_API_HASH -t myapp . +caprover deploy --appName myapp --imageName myapp \ No newline at end of file diff --git a/package.json b/package.json index 584ea95a8..bba1283b1 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,14 @@ { "name": "teledrive", - "version": "2.5.2", + "version": "2.5.4", "repository": "git@github.com:mgilangjanuar/teledrive.git", "author": "M Gilang Januar ", "license": "MIT", "private": true, + "engines": { + "node": "16.14.0" + }, + "scripts": { "web": "yarn workspace web", "server": "yarn workspace api", diff --git a/web/src/pages/dashboard/components/Rename.tsx b/web/src/pages/dashboard/components/Rename.tsx index 2686955dd..39ab9f892 100644 --- a/web/src/pages/dashboard/components/Rename.tsx +++ b/web/src/pages/dashboard/components/Rename.tsx @@ -25,32 +25,40 @@ const Rename: React.FC = ({ const renameFile = async () => { setLoadingRename(true) - const { name } = formRename.getFieldsValue() + const name = String(formRename.getFieldsValue().name) try { + const { data: exists } = await req.get('/files', { params: { parent_id: fileRename.parent_id, name: name } }) + if (/\.part0*\d*$/.test(name)) + throw { status: 400, body: { error: 'The file name cannot end with ".part", even if followed by digits!' } } + if (/\(\d+\).+/.test(name)) + throw { status: 400, body: { error: 'The file name cannot contain text after parentheses with digits inside!' } } + if (exists.length > 0) + throw { status: 400, body: { error: `A file/folder named "${name}" already exists!` } } + const { data: result } = await req.patch(`/files/${fileRename?.id}`, { file: { name } }) notification.success({ message: 'Success', - description: `${name} renamed successfully!` + description: `${fileRename?.name.replace(/\.part0*\d+$/, '')} renamed successfully!` }) dataSource?.[1](dataSource?.[0].map((datum: any) => datum.id === result.file.id ? { ...datum, name } : datum)) setFileRename(undefined) setLoadingRename(false) formRename.resetFields() onFinish?.() - } catch (error) { + } catch (error: any) { setLoadingRename(false) return notification.error({ message: 'Error', - description: 'Failed to rename a file. Please try again!' + description: error?.body?.error || 'Failed to rename a file. Please try again!' }) } } return setFileRename(undefined)} - okText="Add" + okText="Rename" title={Rename {fileRename?.name.replace(/\.part0*\d+$/, '')}} onOk={() => formRename.submit()} cancelButtonProps={{ shape: 'round' }} diff --git a/web/src/pages/dashboard/components/Upload.tsx b/web/src/pages/dashboard/components/Upload.tsx index d7b93aa01..107547d68 100644 --- a/web/src/pages/dashboard/components/Upload.tsx +++ b/web/src/pages/dashboard/components/Upload.tsx @@ -69,6 +69,14 @@ const Upload: React.FC = ({ dataFileList: [fileList, setFileList], parent let deleted = false try { + const { data: exists } = await req.get('/files', { params: { parent_id: parent?.id, name: file.name } }) + if (/\.part0*\d*$/.test(file.name)) + throw { status: 400, body: { error: 'The file name cannot end with ".part", even if followed by digits!' } } + if (/\(\d+\).+/.test(file.name)) + throw { status: 400, body: { error: 'The file name cannot contain text after parentheses with digits inside!' } } + if (exists.length > 0) + throw { status: 400, body: { error: `A file/folder named "${file.name}" already exists!` } } + while (filesWantToUpload.current?.findIndex(f => f.uid === file.uid) !== 0) { await new Promise(res => setTimeout(res, 1000)) } diff --git a/web/src/pages/dashboard/index.tsx b/web/src/pages/dashboard/index.tsx index 9adb5167b..b54ce43f0 100644 --- a/web/src/pages/dashboard/index.tsx +++ b/web/src/pages/dashboard/index.tsx @@ -272,8 +272,7 @@ const Dashboard: React.FC = ({ match }) const name = `Link of ${row.name}` await req.post('/files/addFolder', { file: { ...row, name, link_id: row.id, parent_id: p?.link_id || p?.id, id: undefined } }) } else { - const name = data?.find(datum => datum.name === row.name) ? `Copy of ${row.name}` : row.name - await req.post('/files/cloneFile', { file: { ...row, name, parent_id: p?.link_id || p?.id, id: undefined } }) + await req.post('/files/cloneFile', { file: { ...row, name: row.name, parent_id: p?.link_id || p?.id, id: undefined } }) } })) } else if ((act || action) === 'cut') { diff --git a/web/src/utils/Download.ts b/web/src/utils/Download.ts index 13e34eafb..296203b18 100644 --- a/web/src/utils/Download.ts +++ b/web/src/utils/Download.ts @@ -2,77 +2,70 @@ import streamSaver from 'streamsaver' import { Api } from 'telegram' import { req } from './Fetcher' import { telegramClient } from './Telegram' - +async function downloadFile(client, file) { + let chat + if (file.forward_info && file.forward_info.match(/^channel\//gi)) { + const [type, peerId, id, accessHash] = file.forward_info.split('/') + let peer: Api.InputPeerChannel | Api.InputPeerUser | Api.InputPeerChat + if (type === 'channel') { + peer = new Api.InputPeerChannel({ + channelId: BigInt(peerId) as any, + accessHash: BigInt(accessHash as string) as any, + }) + chat = await client.invoke( + new Api.channels.GetMessages({ + channel: peer, + id: [new Api.InputMessageID({ id: Number(id) })], + }) + ) + } + } else { + chat = await client.invoke( + new Api.messages.GetMessages({ + id: [new Api.InputMessageID({ id: Number(file.message_id) })], + }) + ) + } + return client.downloadMedia(chat['messages'][0].media, { + outputFile: { + write: (chunk: Buffer) => { + return true + }, + }, + }) +} export async function download(id: string): Promise { - const { data: response } = await req.get(`/files/${id}`, { params: { raw: 1, as_array: 1 } }) - let cancel = false + const { data: response } = await req.get(`/files/${id}`, { + params: { raw: 1, as_array: 1 }, + }) const client = await telegramClient.connect() + const filesToDownload = response.files const readableStream = new ReadableStream({ - start(_controller: ReadableStreamDefaultController) { - }, - async pull(controller: ReadableStreamDefaultController) { - let countFiles = 1 - console.log('start downloading:', response.files) - for (const file of response.files) { - let chat: any - if (file.forward_info && file.forward_info.match(/^channel\//gi)) { - const [type, peerId, id, accessHash] = file.forward_info.split('/') - let peer: Api.InputPeerChannel | Api.InputPeerUser | Api.InputPeerChat - if (type === 'channel') { - peer = new Api.InputPeerChannel({ - channelId: BigInt(peerId) as any, - accessHash: BigInt(accessHash as string) as any - }) - chat = await client.invoke(new Api.channels.GetMessages({ - channel: peer, - id: [new Api.InputMessageID({ id: Number(id) })] - })) - } - } else { - chat = await client.invoke(new Api.messages.GetMessages({ - id: [new Api.InputMessageID({ id: Number(file.message_id) })] - })) - } - const getData = async () => await client.downloadMedia(chat['messages'][0].media, { - outputFile: { - write: (chunk: Buffer) => { - if (cancel) return false - return controller.enqueue(chunk) - }, - close: () => { - if (countFiles++ >= Number(response.files.length)) controller.close() - } - }, - progressCallback: (received, total) => { - console.log('progress: ', (Number(received) / Number(total) * 100).toFixed(2), '%') - } - }) + async pull(controller) { + for (const [index, file] of filesToDownload.entries()) { try { - await getData() + const data = await downloadFile(client, file) + controller.enqueue(data) + if (index === filesToDownload.length - 1) { + controller.close() + } } catch (error) { - console.log(error) + console.error(error) } } }, - cancel() { - cancel = true - } - }, { - size(chunk: any) { - return chunk.length - } }) return readableStream } - export const directDownload = async (id: string, name: string): Promise => { const fileStream = streamSaver.createWriteStream(name) const writer = fileStream.getWriter() const reader = (await download(id)).getReader() - const pump = () => reader.read().then(({ value, done }) => { + const pump = async () => { + const { value, done } = await reader.read() if (done) return writer.close() - writer.write(value) - return writer.ready.then(pump) - }) + await writer.write(value) + await pump() + } await pump() } \ No newline at end of file