Skip to content
This repository has been archived by the owner on Apr 23, 2024. It is now read-only.

Commit

Permalink
Merge pull request #157 from mgilangjanuar/staging
Browse files Browse the repository at this point in the history
Release v1.2.0
  • Loading branch information
mgilangjanuar authored Jan 3, 2022
2 parents 1cdd05a + 7f1bab5 commit 9c70c8e
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 107 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "teledrive",
"version": "1.1.9",
"version": "1.2.0",
"repository": "git@github.com:mgilangjanuar/teledrive.git",
"author": "M Gilang Januar <mgilangjanuar@gmail.com>",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "server",
"version": "1.1.9",
"version": "1.2.0",
"main": "dist/index.js",
"license": "MIT",
"private": true,
Expand Down
159 changes: 91 additions & 68 deletions server/src/api/v1/Files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,24 @@ export class Files {
}
file.signed_key = file.signed_key || parent?.signed_key

let files = [file]
if (/.*\.part1$/gi.test(file?.name)) {
if (req.user?.plan !== 'premium') {
throw { status: 402, body: { error: 'Payment required' } }
}
files = await Model.createQueryBuilder('files')
.where(`id = :id or name like '${file.name.replace(/\.part1$/gi, '')}%'`, { id })
.addSelect('files.signed_key')
.orderBy('created_at')
.getMany()
}

if (!req.user || file.user_id !== req.user?.id) {
await Files.initiateSessionTG(req, file)
await Files.initiateSessionTG(req, files)
await req.tg.connect()
}

return await Files.download(req, res, file)
return await Files.download(req, res, files)
}

@Endpoint.DELETE('/:id', { middlewares: [Auth] })
Expand Down Expand Up @@ -257,6 +269,10 @@ export class Files {
throw { status: 400, body: { error: 'Maximum file part size is 500kB' } }
}

if ((!req.user?.plan || req.user?.plan === 'free') && /\.part\d+$/gi.test(name)) {
throw { status: 402, body: { error: 'Payment required' } }
}

let model: Model
if (req.params?.id) {
model = await Model.createQueryBuilder('files')
Expand All @@ -283,7 +299,7 @@ export class Files {
model = new Model()
model.name = name,
model.mime_type = mimetype
model.size = size
model.size = '0'
model.user_id = req.user.id
model.type = type
model.parent_id = parentId as string || null
Expand Down Expand Up @@ -315,7 +331,10 @@ export class Files {
}
}

const { affected } = await Model.update(model.id, { upload_progress: (Number(part) + 1) / Number(totalPart) }, { reload: true })
const { affected } = await Model.update(model.id, {
size: bigInt(model.size).add(file.buffer.length).toString(),
upload_progress: (Number(part) + 1) / Number(totalPart)
}, { reload: true })
if (!affected) {
await Model.delete(model.id)
throw { status: 404, body: { error: 'File not found' } }
Expand Down Expand Up @@ -444,7 +463,7 @@ export class Files {
return res.send({ files })
}

public static async download(req: Request, res: Response, file: Model): Promise<any> {
public static async download(req: Request, res: Response, files: Model[]): Promise<any> {
const { raw, dl, thumb } = req.query

let usage = await Usages.findOne({ where: { key: req.user ? `u:${req.user.id}` : `ip:${req.headers['cf-connecting-ip'] as string || req.ip}` } })
Expand All @@ -462,95 +481,99 @@ export class Files {
await usage.save()
}

const totalFileSize = files.reduce((res, file) => res.add(file.size), bigInt(0))
if (!req.user || !req.user.plan || req.user.plan === 'free') { // not expired and free plan
// check quota
if (bigInt(usage.usage).add(bigInt(file.size)).greater(1_500_000_000)) {
if (bigInt(usage.usage).add(bigInt(totalFileSize)).greater(1_500_000_000)) {
throw { status: 402, body: { error: 'Payment required' } }
}
}

if (!raw || Number(raw) === 0) {
const { signed_key: _, ...result } = file
const { signed_key: _, ...result } = files[0]
return res.send({ file: result })
}

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),
accessHash: bigInt(accessHash as string) })
chat = await req.tg.invoke(new Api.channels.GetMessages({
channel: peer,
id: [new Api.InputMessageID({ id: Number(id) })]
}))
}
} else {
chat = await req.tg.invoke(new Api.messages.GetMessages({
id: [new Api.InputMessageID({ id: Number(file.message_id) })]
}))
}

let cancel = false
req.on('close', () => cancel = true)
res.setHeader('Content-Disposition', contentDisposition(files[0].name.replace(/\.part1$/gi, ''), { type: Number(dl) === 1 ? 'attachment' : 'inline' }))
res.setHeader('Content-Type', files[0].mime_type)
res.setHeader('Content-Length', totalFileSize.toString())

res.setHeader('Content-Disposition', contentDisposition(file.name, { type: Number(dl) === 1 ? 'attachment' : 'inline' }))
res.setHeader('Content-Type', file.mime_type)
res.setHeader('Content-Length', file.size)
let data = null

const chunk = 512 * 1024
let idx = 0

while (!cancel && data === null || data.length && bigInt(file.size).greater(bigInt(idx * chunk))) {
// const startDate = Date.now()
const getData = async () => await req.tg.downloadMedia(chat['messages'][0].media, {
...thumb ? { sizeType: 'i' } : {},
start: idx++ * chunk,
end: bigInt.min(bigInt(file.size), bigInt(idx * chunk - 1)).toJSNumber(),
workers: 1, // using 1 for stable
progressCallback: (() => {
const updateProgess: any = () => {
updateProgess.isCanceled = cancel
}
return updateProgess
})()
})
try {
data = await getData()
res.write(data)
} catch (error) {
for (const file of 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),
accessHash: bigInt(accessHash as string) })
chat = await req.tg.invoke(new Api.channels.GetMessages({
channel: peer,
id: [new Api.InputMessageID({ id: Number(id) })]
}))
}
} else {
chat = await req.tg.invoke(new Api.messages.GetMessages({
id: [new Api.InputMessageID({ id: Number(file.message_id) })]
}))
}

let data = null

const chunk = 512 * 1024
let idx = 0

while (!cancel && data === null || data.length && bigInt(file.size).greater(bigInt(idx * chunk))) {
// const startDate = Date.now()
const getData = async () => await req.tg.downloadMedia(chat['messages'][0].media, {
...thumb ? { sizeType: 'i' } : {},
start: idx++ * chunk,
end: bigInt.min(bigInt(file.size), bigInt(idx * chunk - 1)).toJSNumber(),
workers: 1, // using 1 for stable
progressCallback: (() => {
const updateProgess: any = () => {
updateProgess.isCanceled = cancel
}
return updateProgess
})()
})
try {
await new Promise(resolve => setTimeout(resolve, 1000))
await req.tg?.connect()
const data = await getData()
data = await getData()
res.write(data)
} catch (error) {
await new Promise(resolve => setTimeout(resolve, 1000))
await req.tg?.connect()
const data = await getData()
res.write(data)
try {
await new Promise(resolve => setTimeout(resolve, 1000))
await req.tg?.connect()
const data = await getData()
res.write(data)
} catch (error) {
await new Promise(resolve => setTimeout(resolve, 1000))
await req.tg?.connect()
const data = await getData()
res.write(data)
}
}
// if (!req.user?.plan || req.user?.plan === 'free') {
// await new Promise(res => setTimeout(res, 1000 - (Date.now() - startDate))) // bandwidth 512 kbsp
// }
}
// if (!req.user?.plan || req.user?.plan === 'free') {
// await new Promise(res => setTimeout(res, 1000 - (Date.now() - startDate))) // bandwidth 512 kbsp
// }
usage.usage = bigInt(file.size).add(bigInt(usage.usage)).toString()
await usage.save()
}
usage.usage = bigInt(file.size).add(bigInt(usage.usage)).toString()
await usage.save()

res.end()
}

public static async initiateSessionTG(req: Request, file?: Model): Promise<Model> {
if (!file) {
public static async initiateSessionTG(req: Request, files?: Model[]): Promise<Model[]> {
if (!files?.length) {
throw { status: 404, body: { error: 'File not found' } }
}

let data: { file: { id: string }, session: string }
try {
data = JSON.parse(AES.decrypt(file.signed_key, process.env.FILES_JWT_SECRET).toString(enc.Utf8))
data = JSON.parse(AES.decrypt(files[0].signed_key, process.env.FILES_JWT_SECRET).toString(enc.Utf8))
} catch (error) {
throw { status: 401, body: { error: 'Invalid token' } }
}
Expand All @@ -561,6 +584,6 @@ export class Files {
} catch (error) {
throw { status: 401, body: { error: 'Invalid key' } }
}
return file
return files
}
}
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "1.1.9",
"version": "1.2.0",
"private": true,
"dependencies": {
"@craco/craco": "^6.3.0",
Expand Down
14 changes: 10 additions & 4 deletions web/src/pages/Pricing.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ArrowRightOutlined, QuestionCircleOutlined } from '@ant-design/icons'
import { ArrowRightOutlined } from '@ant-design/icons'
import { Button, Card, Col, Divider, Form, Input, Layout, notification, Row, Switch, Tooltip, Typography } from 'antd'
import React, { useState } from 'react'
import { useHistory } from 'react-router'
Expand Down Expand Up @@ -44,14 +44,19 @@ const Pricing: React.FC = () => {
<li><strong>Unlimited</strong> total files</li>
<li><strong>Unlimited</strong> total files size</li>
<li><strong>1.5GB</strong> daily bandwidth</li>
<li><strong>2GB</strong> upload &amp; download max</li>
<li><strong>All basic features</strong></li>
</ul>
</Card>

const Premium = () => <Card color="warning" hoverable title="Premium" style={{ fontSize: '1rem' }} actions={[
<>
{isIDR && <Form.Item style={{ margin: '15px 20px' }} label={<>Email &nbsp; <Tooltip placement="topLeft" title="This is required for sending the invoice to your email"><QuestionCircleOutlined /></Tooltip></>}>
<Input.Search type="email" size="large" placeholder="Type your email here..." required defaultValue={email} onBlur={({ target }) => setEmail(target.value)} enterButton={<ArrowRightOutlined />} onSearch={val => select('premium', 'midtrans', val)} />
{isIDR && <Form.Item style={{ margin: '15px 20px' }}>
<Tooltip placement="topLeft" title="This is required for sending the invoice to your email">
<Input.Search type="email" size="large" placeholder="Type your email here..." required defaultValue={email}
onBlur={({ target }) => setEmail(target.value)} enterButton={<ArrowRightOutlined />}
onSearch={val => select('premium', 'midtrans', val)} />
</Tooltip>
</Form.Item>}
<Button block loading={loading} type="text" size="large" onClick={() => isIDR ? select('premium', 'midtrans') : select('premium')}>
{isIDR ? <>Powered by<strong> Midtrans</strong></> : <>Subscribe with<strong> PayPal</strong></>} <ArrowRightOutlined />
Expand All @@ -70,6 +75,7 @@ const Pricing: React.FC = () => {
<li><strong>Unlimited</strong> total files</li>
<li><strong>Unlimited</strong> total files size</li>
<li><strong>Unlimited</strong> bandwidth usage</li>
<li><strong>Unlimited</strong> upload &amp; download</li>
<li><strong>All features</strong></li>
</ul>
</Card>
Expand Down Expand Up @@ -126,4 +132,4 @@ const Pricing: React.FC = () => {
</>
}

export default Pricing
export default Pricing
Loading

0 comments on commit 9c70c8e

Please sign in to comment.