dragxd commited on
Commit
e1a2a92
·
0 Parent(s):

Initial commit

Browse files
Files changed (46) hide show
  1. .gitattributes +2 -0
  2. .gitignore +8 -0
  3. Dockerfile +18 -0
  4. LICENSE +21 -0
  5. README.md +170 -0
  6. config.py +60 -0
  7. docker_start.sh +3 -0
  8. main.py +356 -0
  9. render.yaml +24 -0
  10. requirements.txt +11 -0
  11. runtime.txt +1 -0
  12. sample.env +7 -0
  13. start_main.py +3 -0
  14. utils/bot_mode.py +197 -0
  15. utils/clients.py +113 -0
  16. utils/directoryHandler.py +387 -0
  17. utils/downloader.py +85 -0
  18. utils/extra.py +124 -0
  19. utils/logger.py +48 -0
  20. utils/streamer/__init__.py +82 -0
  21. utils/streamer/custom_dl.py +199 -0
  22. utils/streamer/file_properties.py +84 -0
  23. utils/uploader.py +69 -0
  24. website/VideoPlayer.html +123 -0
  25. website/home.html +158 -0
  26. website/static/assets/file-icon.svg +1 -0
  27. website/static/assets/folder-icon.svg +1 -0
  28. website/static/assets/folder-solid-icon.svg +1 -0
  29. website/static/assets/home-icon.svg +1 -0
  30. website/static/assets/info-icon-small.svg +1 -0
  31. website/static/assets/link-icon.svg +1 -0
  32. website/static/assets/load-icon.svg +1 -0
  33. website/static/assets/more-icon.svg +1 -0
  34. website/static/assets/pencil-icon.svg +1 -0
  35. website/static/assets/plus-icon.svg +1 -0
  36. website/static/assets/profile-icon.svg +1 -0
  37. website/static/assets/search-icon.svg +1 -0
  38. website/static/assets/share-icon.svg +1 -0
  39. website/static/assets/trash-icon.svg +1 -0
  40. website/static/assets/upload-icon.svg +1 -0
  41. website/static/home.css +527 -0
  42. website/static/js/apiHandler.js +347 -0
  43. website/static/js/extra.js +119 -0
  44. website/static/js/fileClickHandler.js +214 -0
  45. website/static/js/main.js +95 -0
  46. website/static/js/sidebar.js +102 -0
.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
.gitignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ *.pyc
2
+ *.data
3
+ t.py
4
+ t2.py
5
+ cache
6
+ config copy.py
7
+ logs.txt
8
+ .env
Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use the official Python base image
2
+ FROM python:3.11.7-slim AS base
3
+
4
+ # Set the working directory inside the container
5
+ WORKDIR /app
6
+
7
+ # Copy the requirements file to the working directory and install dependencies
8
+ COPY requirements.txt .
9
+ RUN pip install --no-cache-dir -r requirements.txt
10
+
11
+ # Copy the application code to the working directory
12
+ COPY . .
13
+
14
+ # Expose the port on which the application will run
15
+ EXPOSE 8080
16
+
17
+ # Run the FastAPI application using uvicorn server
18
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2024 TechShreyash
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TGDrive - A Google Drive Clone with Telegram Storage
2
+
3
+ Welcome to TGDrive! This web application replicates Google Drive's functionalities using Telegram as its storage backend. Manage folders and files, perform actions like uploading, renaming, and deleting, utilize trash/bin support, enable permanent deletion, and share public links. The application offers admin login and automatically backs up the database to Telegram.
4
+
5
+ **Check out the [TGDrive Personal](https://github.com/TechShreyash/TGDrivePersonal) project for local desktop backup support.**
6
+
7
+ ## Features
8
+
9
+ - **File Management:** Upload, rename, and delete files with integrated trash/bin functionality and permanent deletion support.
10
+ - **Folder Management:** Easily create, rename, and delete folders.
11
+ - **Sharing:** Seamlessly share public links for files and folders.
12
+ - **Admin Support:** Secure admin login for efficient management.
13
+ - **Automatic Backups:** Automated database backups sent directly to Telegram.
14
+ - **Multiple Bots/Clients:** Support for multiple bots/clients for file operations and streaming from Telegram.
15
+ - **Large File Support:** Upload files up to 4GB using Telegram Premium accounts.
16
+ - **Auto Pinger:** Built-in feature to keep the website active by preventing idle timeouts.
17
+ - **URL Upload Support:** Upload files directly to TG Drive from any direct download link of a file.
18
+ - **Bot Mode:** Upload files directly to any folder in TG Drive by sending the file to the bot on Telegram ([Know More](#tg-drives-bot-mode))
19
+
20
+ ## Tech Stack
21
+
22
+ - **Backend:** Python, FastAPI
23
+ - **Frontend:** HTML, CSS, JavaScript
24
+ - **Database:** Local storage as a class object, saved to a file using the pickle module.
25
+ - **Storage:** Telegram
26
+
27
+ ### Environment Variables
28
+
29
+ #### Required Variables
30
+
31
+ | Variable Name | Type | Example | Description |
32
+ | ------------------------ | ------- | ------------------------- | -------------------------------------------------------------------- |
33
+ | `API_ID` | integer | 123456 | Your Telegram API ID |
34
+ | `API_HASH` | string | dagsjdhgjfsahgjfh | Your Telegram API Hash |
35
+ | `BOT_TOKENS` | string | 21413535:gkdshajfhjfakhjf | List of Telegram bot tokens for file operations, separated by commas |
36
+ | `STORAGE_CHANNEL` | integer | -100123456789 | Chat ID of the Telegram storage channel |
37
+ | `DATABASE_BACKUP_MSG_ID` | integer | 123 | Message ID of a file in the storage channel for database backups |
38
+
39
+ > Note: All bots mentioned in the `BOT_TOKENS` variable must be added as admins in your `STORAGE_CHANNEL`.
40
+
41
+ > Note: `DATABASE_BACKUP_MSG_ID` should be the message ID of a file (document) in the `STORAGE_CHANNEL`.
42
+
43
+ #### Optional Variables
44
+
45
+ | Variable Name | Type | Default | Description |
46
+ | ---------------------- | -------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
47
+ | `ADMIN_PASSWORD` | string | admin | Password for accessing the admin panel |
48
+ | `STRING_SESSIONS` | string | None | List of Premium Telegram Account Pyrogram String Sessions for file operations |
49
+ | `SLEEP_THRESHOLD` | integer (in seconds) | 60 | Delay in seconds before retrying after a Telegram API floodwait error |
50
+ | `DATABASE_BACKUP_TIME` | integer (in seconds) | 60 | Interval in seconds for database backups to the storage channel |
51
+ | `MAX_FILE_SIZE` | float (in GBs) | 1.98 (3.98 if `STRING_SESSIONS` are added) | Maximum file size (in GBs) allowed for uploading to Telegram |
52
+ | `WEBSITE_URL` | string | None | Website URL (with https/http) to auto-ping to keep the website active |
53
+ | `MAIN_BOT_TOKEN` | string | None | Your Main Bot Token to use [TG Drive's Bot Mode](#tg-drives-bot-mode) |
54
+ | `TELEGRAM_ADMIN_IDS` | string | None | List of Telegram User IDs of admins who can access the [bot mode](#tg-drives-bot-mode), separated by commas |
55
+
56
+ > Note: Premium Client (`STRING_SESSIONS`) will be used only to upload files when file size is greater than 2GB.
57
+
58
+ > Note: File streaming/downloads will be handled by bots (`BOT_TOKENS`).
59
+
60
+ > Note: Read more about TG Drive's Bot Mode [here](#tg-drives-bot-mode).
61
+
62
+ ## Deploying Your Own TG Drive Application
63
+
64
+ ### 1. Clone the Repository
65
+
66
+ First, clone the repository and navigate into the project directory:
67
+
68
+ ```bash
69
+ git clone https://github.com/TechShreyash/TGDrive
70
+ cd TGDrive
71
+ ```
72
+
73
+ ### 2. Set Up Your Environment Variables
74
+
75
+ Create a `.env` file in the root directory and add the necessary [environment variables](#environment-variables).
76
+
77
+ > **Note:** Some hosting services allow you to set environment variables directly through their interface, which may eliminate the need for a `.env` file.
78
+
79
+ ### 3. Running TG Drive
80
+
81
+ #### Deploying Locally
82
+
83
+ 1. Install the required Python packages:
84
+
85
+ ```bash
86
+ pip install -U -r requirements.txt
87
+ ```
88
+
89
+ 2. Start the TG Drive application using Uvicorn:
90
+
91
+ ```bash
92
+ uvicorn main:app --host 0.0.0.0 --port 8000
93
+ ```
94
+
95
+ #### Deploying Using Docker
96
+
97
+ 1. Build the Docker image:
98
+
99
+ ```bash
100
+ docker build -t tgdrive .
101
+ ```
102
+
103
+ 2. Run the Docker container:
104
+
105
+ ```bash
106
+ docker run -d -p 8000:8000 tgdrive
107
+ ```
108
+
109
+ Access the application at `http://127.0.0.1:8000` or `http://your_ip:8000`.
110
+
111
+ > **Note:** For more detailed information on deploying FastAPI applications, refer to online guides and resources.
112
+
113
+ ## Deploy Tutorials
114
+
115
+ **Deploy To Render.com For Free :** https://youtu.be/S5OIi5Ur3c0
116
+
117
+ <div align="center">
118
+
119
+ [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/TechShreyash/TGDrive)
120
+
121
+ </div>
122
+
123
+ > **Note:** After updating the TG Drive code, clear your browser's cache to ensure the latest JavaScript files are loaded and run correctly.
124
+
125
+ ## TG Drive's Bot Mode
126
+
127
+ TG Drive's Bot Mode is a new feature that allows you to upload files directly to your TG Drive website from a Telegram bot. Simply send or forward any file to the bot, and it will be uploaded to your TG Drive. You can also specify the folder where you want the files to be uploaded.
128
+
129
+ To use this feature, you need to set the configuration variables `MAIN_BOT_TOKEN` and `TELEGRAM_ADMIN_IDS`. More information about these variables can be found in the [optional variables section](#optional-variables).
130
+
131
+ Once these variables are set, users whose IDs are listed in `TELEGRAM_ADMIN_IDS` will have access to the bot.
132
+
133
+ ### Bot Commands
134
+
135
+ - `/set_folder` - Set the folder for file uploads
136
+ - `/current_folder` - Check the current folder
137
+
138
+ ### Quick Demo
139
+
140
+ Bot Mode - Youtube Video Tutorial : https://youtu.be/XSeY2XcHdGI
141
+
142
+ #### Uploading Files
143
+
144
+ 1. Open your main bot in Telegram.
145
+ 2. Send or forward a file to this bot, and it will be uploaded. By default, the file will be uploaded to the root folder (home page).
146
+
147
+ #### Changing Folder for Uploading
148
+
149
+ 1. Send the `/set_folder` command and follow the instructions provided by the bot.
150
+
151
+ ## Important Posts Regarding TG Drive
152
+
153
+ Stay informed by joining our updates channel on Telegram: [@TechZBots](https://telegram.me/TechZBots). We post updates, guides, and tips about TG Drive there.
154
+
155
+ - https://telegram.me/TechZBots/891
156
+ - https://telegram.me/TechZBots/876
157
+ - https://telegram.me/TechZBots/874
158
+ - https://telegram.me/TechZBots/870
159
+
160
+ ## Contributing
161
+
162
+ Contributions are welcome! Fork the repository, make your changes, and create a pull request.
163
+
164
+ ## License
165
+
166
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
167
+
168
+ ## Support
169
+
170
+ For inquiries or support, join our [Telegram Support Group](https://telegram.me/TechZBots_Support) or email [techshreyash123@gmail.com](mailto:techshreyash123@gmail.com).
config.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dotenv import load_dotenv
2
+ import os
3
+
4
+ # Load environment variables from the .env file, if present
5
+ load_dotenv()
6
+
7
+ # Telegram API credentials obtained from https://my.telegram.org/auth
8
+ API_ID = int(os.getenv("API_ID")) # Your Telegram API ID
9
+ API_HASH = os.getenv("API_HASH") # Your Telegram API Hash
10
+
11
+ # List of Telegram bot tokens used for file upload/download operations
12
+ BOT_TOKENS = os.getenv("BOT_TOKENS", "").strip(", ").split(",")
13
+ BOT_TOKENS = [token.strip() for token in BOT_TOKENS if token.strip() != ""]
14
+
15
+ # List of Premium Telegram Account Pyrogram String Sessions used for file upload/download operations
16
+ STRING_SESSIONS = os.getenv("STRING_SESSIONS", "").strip(", ").split(",")
17
+ STRING_SESSIONS = [
18
+ session.strip() for session in STRING_SESSIONS if session.strip() != ""
19
+ ]
20
+
21
+ # Chat ID of the Telegram storage channel where files will be stored
22
+ STORAGE_CHANNEL = int(os.getenv("STORAGE_CHANNEL")) # Your storage channel's chat ID
23
+
24
+ # Message ID of a file in the storage channel used for storing database backups
25
+ DATABASE_BACKUP_MSG_ID = int(
26
+ os.getenv("DATABASE_BACKUP_MSG_ID")
27
+ ) # Message ID for database backup
28
+
29
+ # Password used to access the website's admin panel
30
+ ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "admin") # Default to "admin" if not set
31
+
32
+ # Determine the maximum file size (in bytes) allowed for uploading to Telegram
33
+ # 1.98 GB if no premium sessions are provided, otherwise 3.98 GB
34
+ if len(STRING_SESSIONS) == 0:
35
+ MAX_FILE_SIZE = 1.98 * 1024 * 1024 * 1024 # 2 GB in bytes
36
+ else:
37
+ MAX_FILE_SIZE = 3.98 * 1024 * 1024 * 1024 # 4 GB in bytes
38
+
39
+ # Database backup interval in seconds. Backups will be sent to the storage channel at this interval
40
+ DATABASE_BACKUP_TIME = int(
41
+ os.getenv("DATABASE_BACKUP_TIME", 60)
42
+ ) # Default to 60 seconds
43
+
44
+ # Time delay in seconds before retrying after a Telegram API floodwait error
45
+ SLEEP_THRESHOLD = int(os.getenv("SLEEP_THRESHOLD", 60)) # Default to 60 seconds
46
+
47
+ # Domain to auto-ping and keep the website active
48
+ WEBSITE_URL = os.getenv("WEBSITE_URL", None)
49
+
50
+
51
+ # For Using TG Drive's Bot Mode
52
+
53
+ # Main Bot Token for TG Drive's Bot Mode
54
+ MAIN_BOT_TOKEN = os.getenv("MAIN_BOT_TOKEN", "")
55
+ if MAIN_BOT_TOKEN.strip() == "":
56
+ MAIN_BOT_TOKEN = None
57
+
58
+ # List of Telegram User IDs who have admin access to the bot mode
59
+ TELEGRAM_ADMIN_IDS = os.getenv("TELEGRAM_ADMIN_IDS", "").strip(", ").split(",")
60
+ TELEGRAM_ADMIN_IDS = [int(id) for id in TELEGRAM_ADMIN_IDS if id.strip() != ""]
docker_start.sh ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ docker stop tgdrive
2
+ docker build -t tgdrive .
3
+ docker run -d --name tgdrive -p 80:80 tgdrive
main.py ADDED
@@ -0,0 +1,356 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from utils.downloader import (
2
+ download_file,
3
+ get_file_info_from_url,
4
+ )
5
+ import asyncio
6
+ from pathlib import Path
7
+ from contextlib import asynccontextmanager
8
+ import aiofiles
9
+ from fastapi import FastAPI, HTTPException, Request, File, UploadFile, Form, Response
10
+ from fastapi.responses import FileResponse, JSONResponse
11
+ from config import ADMIN_PASSWORD, MAX_FILE_SIZE, STORAGE_CHANNEL
12
+ from utils.clients import initialize_clients
13
+ from utils.directoryHandler import getRandomID
14
+ from utils.extra import auto_ping_website, convert_class_to_dict, reset_cache_dir
15
+ from utils.streamer import media_streamer
16
+ from utils.uploader import start_file_uploader
17
+ from utils.logger import Logger
18
+ import urllib.parse
19
+
20
+
21
+ # Startup Event
22
+ @asynccontextmanager
23
+ async def lifespan(app: FastAPI):
24
+ # Reset the cache directory, delete cache files
25
+ reset_cache_dir()
26
+
27
+ # Initialize the clients
28
+ await initialize_clients()
29
+
30
+ # Start the website auto ping task
31
+ asyncio.create_task(auto_ping_website())
32
+
33
+ yield
34
+
35
+
36
+ app = FastAPI(docs_url=None, redoc_url=None, lifespan=lifespan)
37
+ logger = Logger(__name__)
38
+
39
+
40
+ @app.get("/")
41
+ async def home_page():
42
+ return FileResponse("website/home.html")
43
+
44
+
45
+ @app.get("/stream")
46
+ async def home_page():
47
+ return FileResponse("website/VideoPlayer.html")
48
+
49
+
50
+ @app.get("/static/{file_path:path}")
51
+ async def static_files(file_path):
52
+ if "apiHandler.js" in file_path:
53
+ with open(Path("website/static/js/apiHandler.js")) as f:
54
+ content = f.read()
55
+ content = content.replace("MAX_FILE_SIZE__SDGJDG", str(MAX_FILE_SIZE))
56
+ return Response(content=content, media_type="application/javascript")
57
+ return FileResponse(f"website/static/{file_path}")
58
+
59
+
60
+ @app.get("/file")
61
+ async def dl_file(request: Request):
62
+ from utils.directoryHandler import DRIVE_DATA
63
+
64
+ path = request.query_params["path"]
65
+ file = DRIVE_DATA.get_file(path)
66
+ return await media_streamer(STORAGE_CHANNEL, file.file_id, file.name, request)
67
+
68
+
69
+ # Api Routes
70
+
71
+
72
+ @app.post("/api/checkPassword")
73
+ async def check_password(request: Request):
74
+ data = await request.json()
75
+ if data["pass"] == ADMIN_PASSWORD:
76
+ return JSONResponse({"status": "ok"})
77
+ return JSONResponse({"status": "Invalid password"})
78
+
79
+
80
+ @app.post("/api/createNewFolder")
81
+ async def api_new_folder(request: Request):
82
+ from utils.directoryHandler import DRIVE_DATA
83
+
84
+ data = await request.json()
85
+
86
+ if data["password"] != ADMIN_PASSWORD:
87
+ return JSONResponse({"status": "Invalid password"})
88
+
89
+ logger.info(f"createNewFolder {data}")
90
+ folder_data = DRIVE_DATA.get_directory(data["path"]).contents
91
+ for id in folder_data:
92
+ f = folder_data[id]
93
+ if f.type == "folder":
94
+ if f.name == data["name"]:
95
+ return JSONResponse(
96
+ {
97
+ "status": "Folder with the name already exist in current directory"
98
+ }
99
+ )
100
+
101
+ DRIVE_DATA.new_folder(data["path"], data["name"])
102
+ return JSONResponse({"status": "ok"})
103
+
104
+
105
+ @app.post("/api/getDirectory")
106
+ async def api_get_directory(request: Request):
107
+ from utils.directoryHandler import DRIVE_DATA
108
+
109
+ data = await request.json()
110
+
111
+ if data["password"] == ADMIN_PASSWORD:
112
+ is_admin = True
113
+ else:
114
+ is_admin = False
115
+
116
+ auth = data.get("auth")
117
+
118
+ logger.info(f"getFolder {data}")
119
+
120
+ if data["path"] == "/trash":
121
+ data = {"contents": DRIVE_DATA.get_trashed_files_folders()}
122
+ folder_data = convert_class_to_dict(data, isObject=False, showtrash=True)
123
+
124
+ elif "/search_" in data["path"]:
125
+ query = urllib.parse.unquote(data["path"].split("_", 1)[1])
126
+ print(query)
127
+ data = {"contents": DRIVE_DATA.search_file_folder(query)}
128
+ print(data)
129
+ folder_data = convert_class_to_dict(data, isObject=False, showtrash=False)
130
+ print(folder_data)
131
+
132
+ elif "/share_" in data["path"]:
133
+ path = data["path"].split("_", 1)[1]
134
+ folder_data, auth_home_path = DRIVE_DATA.get_directory(path, is_admin, auth)
135
+ auth_home_path= auth_home_path.replace("//", "/") if auth_home_path else None
136
+ folder_data = convert_class_to_dict(folder_data, isObject=True, showtrash=False)
137
+ return JSONResponse(
138
+ {"status": "ok", "data": folder_data, "auth_home_path": auth_home_path}
139
+ )
140
+
141
+ else:
142
+ folder_data = DRIVE_DATA.get_directory(data["path"])
143
+ folder_data = convert_class_to_dict(folder_data, isObject=True, showtrash=False)
144
+ return JSONResponse({"status": "ok", "data": folder_data, "auth_home_path": None})
145
+
146
+
147
+ SAVE_PROGRESS = {}
148
+
149
+
150
+ @app.post("/api/upload")
151
+ async def upload_file(
152
+ file: UploadFile = File(...),
153
+ path: str = Form(...),
154
+ password: str = Form(...),
155
+ id: str = Form(...),
156
+ total_size: str = Form(...),
157
+ ):
158
+ global SAVE_PROGRESS
159
+
160
+ if password != ADMIN_PASSWORD:
161
+ return JSONResponse({"status": "Invalid password"})
162
+
163
+ total_size = int(total_size)
164
+ SAVE_PROGRESS[id] = ("running", 0, total_size)
165
+
166
+ ext = file.filename.lower().split(".")[-1]
167
+
168
+ cache_dir = Path("./cache")
169
+ cache_dir.mkdir(parents=True, exist_ok=True)
170
+ file_location = cache_dir / f"{id}.{ext}"
171
+
172
+ file_size = 0
173
+
174
+ async with aiofiles.open(file_location, "wb") as buffer:
175
+ while chunk := await file.read(1024 * 1024): # Read file in chunks of 1MB
176
+ SAVE_PROGRESS[id] = ("running", file_size, total_size)
177
+ file_size += len(chunk)
178
+ if file_size > MAX_FILE_SIZE:
179
+ await buffer.close()
180
+ file_location.unlink() # Delete the partially written file
181
+ raise HTTPException(
182
+ status_code=400,
183
+ detail=f"File size exceeds {MAX_FILE_SIZE} bytes limit",
184
+ )
185
+ await buffer.write(chunk)
186
+
187
+ SAVE_PROGRESS[id] = ("completed", file_size, file_size)
188
+
189
+ asyncio.create_task(
190
+ start_file_uploader(file_location, id, path, file.filename, file_size)
191
+ )
192
+
193
+ return JSONResponse({"id": id, "status": "ok"})
194
+
195
+
196
+ @app.post("/api/getSaveProgress")
197
+ async def get_save_progress(request: Request):
198
+ global SAVE_PROGRESS
199
+
200
+ data = await request.json()
201
+
202
+ if data["password"] != ADMIN_PASSWORD:
203
+ return JSONResponse({"status": "Invalid password"})
204
+
205
+ logger.info(f"getUploadProgress {data}")
206
+ try:
207
+ progress = SAVE_PROGRESS[data["id"]]
208
+ return JSONResponse({"status": "ok", "data": progress})
209
+ except:
210
+ return JSONResponse({"status": "not found"})
211
+
212
+
213
+ @app.post("/api/getUploadProgress")
214
+ async def get_upload_progress(request: Request):
215
+ from utils.uploader import PROGRESS_CACHE
216
+
217
+ data = await request.json()
218
+
219
+ if data["password"] != ADMIN_PASSWORD:
220
+ return JSONResponse({"status": "Invalid password"})
221
+
222
+ logger.info(f"getUploadProgress {data}")
223
+
224
+ try:
225
+ progress = PROGRESS_CACHE[data["id"]]
226
+ return JSONResponse({"status": "ok", "data": progress})
227
+ except:
228
+ return JSONResponse({"status": "not found"})
229
+
230
+
231
+ @app.post("/api/cancelUpload")
232
+ async def cancel_upload(request: Request):
233
+ from utils.uploader import STOP_TRANSMISSION
234
+ from utils.downloader import STOP_DOWNLOAD
235
+
236
+ data = await request.json()
237
+
238
+ if data["password"] != ADMIN_PASSWORD:
239
+ return JSONResponse({"status": "Invalid password"})
240
+
241
+ logger.info(f"cancelUpload {data}")
242
+ STOP_TRANSMISSION.append(data["id"])
243
+ STOP_DOWNLOAD.append(data["id"])
244
+ return JSONResponse({"status": "ok"})
245
+
246
+
247
+ @app.post("/api/renameFileFolder")
248
+ async def rename_file_folder(request: Request):
249
+ from utils.directoryHandler import DRIVE_DATA
250
+
251
+ data = await request.json()
252
+
253
+ if data["password"] != ADMIN_PASSWORD:
254
+ return JSONResponse({"status": "Invalid password"})
255
+
256
+ logger.info(f"renameFileFolder {data}")
257
+ DRIVE_DATA.rename_file_folder(data["path"], data["name"])
258
+ return JSONResponse({"status": "ok"})
259
+
260
+
261
+ @app.post("/api/trashFileFolder")
262
+ async def trash_file_folder(request: Request):
263
+ from utils.directoryHandler import DRIVE_DATA
264
+
265
+ data = await request.json()
266
+
267
+ if data["password"] != ADMIN_PASSWORD:
268
+ return JSONResponse({"status": "Invalid password"})
269
+
270
+ logger.info(f"trashFileFolder {data}")
271
+ DRIVE_DATA.trash_file_folder(data["path"], data["trash"])
272
+ return JSONResponse({"status": "ok"})
273
+
274
+
275
+ @app.post("/api/deleteFileFolder")
276
+ async def delete_file_folder(request: Request):
277
+ from utils.directoryHandler import DRIVE_DATA
278
+
279
+ data = await request.json()
280
+
281
+ if data["password"] != ADMIN_PASSWORD:
282
+ return JSONResponse({"status": "Invalid password"})
283
+
284
+ logger.info(f"deleteFileFolder {data}")
285
+ DRIVE_DATA.delete_file_folder(data["path"])
286
+ return JSONResponse({"status": "ok"})
287
+
288
+
289
+ @app.post("/api/getFileInfoFromUrl")
290
+ async def getFileInfoFromUrl(request: Request):
291
+
292
+ data = await request.json()
293
+
294
+ if data["password"] != ADMIN_PASSWORD:
295
+ return JSONResponse({"status": "Invalid password"})
296
+
297
+ logger.info(f"getFileInfoFromUrl {data}")
298
+ try:
299
+ file_info = await get_file_info_from_url(data["url"])
300
+ return JSONResponse({"status": "ok", "data": file_info})
301
+ except Exception as e:
302
+ return JSONResponse({"status": str(e)})
303
+
304
+
305
+ @app.post("/api/startFileDownloadFromUrl")
306
+ async def startFileDownloadFromUrl(request: Request):
307
+ data = await request.json()
308
+
309
+ if data["password"] != ADMIN_PASSWORD:
310
+ return JSONResponse({"status": "Invalid password"})
311
+
312
+ logger.info(f"startFileDownloadFromUrl {data}")
313
+ try:
314
+ id = getRandomID()
315
+ asyncio.create_task(
316
+ download_file(data["url"], id, data["path"], data["filename"], data["singleThreaded"])
317
+ )
318
+ return JSONResponse({"status": "ok", "id": id})
319
+ except Exception as e:
320
+ return JSONResponse({"status": str(e)})
321
+
322
+
323
+ @app.post("/api/getFileDownloadProgress")
324
+ async def getFileDownloadProgress(request: Request):
325
+ from utils.downloader import DOWNLOAD_PROGRESS
326
+
327
+ data = await request.json()
328
+
329
+ if data["password"] != ADMIN_PASSWORD:
330
+ return JSONResponse({"status": "Invalid password"})
331
+
332
+ logger.info(f"getFileDownloadProgress {data}")
333
+
334
+ try:
335
+ progress = DOWNLOAD_PROGRESS[data["id"]]
336
+ return JSONResponse({"status": "ok", "data": progress})
337
+ except:
338
+ return JSONResponse({"status": "not found"})
339
+
340
+
341
+ @app.post("/api/getFolderShareAuth")
342
+ async def getFolderShareAuth(request: Request):
343
+ from utils.directoryHandler import DRIVE_DATA
344
+
345
+ data = await request.json()
346
+
347
+ if data["password"] != ADMIN_PASSWORD:
348
+ return JSONResponse({"status": "Invalid password"})
349
+
350
+ logger.info(f"getFolderShareAuth {data}")
351
+
352
+ try:
353
+ auth = DRIVE_DATA.get_folder_auth(data["path"])
354
+ return JSONResponse({"status": "ok", "auth": auth})
355
+ except:
356
+ return JSONResponse({"status": "not found"})
render.yaml ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ # A Docker web service
3
+ - type: web
4
+ name: tgdrive
5
+ repo: https://github.com/TechShreyash/TGDrive
6
+ runtime: python
7
+ branch: main
8
+ plan: free
9
+ autoDeploy: false
10
+ buildCommand: pip install -r requirements.txt
11
+ startCommand: uvicorn main:app --host 0.0.0.0 --port $PORT
12
+ envVars:
13
+ - key: ADMIN_PASSWORD
14
+ sync: false
15
+ - key: API_ID
16
+ sync: false
17
+ - key: API_HASH
18
+ sync: false
19
+ - key: BOT_TOKENS
20
+ sync: false
21
+ - key: STORAGE_CHANNEL
22
+ sync: false
23
+ - key: DATABASE_BACKUP_MSG_ID
24
+ sync: false
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ https://github.com/KurimuzonAkuma/pyrogram/archive/dev.zip
2
+ tgcrypto
3
+ uvicorn
4
+ fastapi[all]
5
+ python-dotenv
6
+ aiofiles
7
+ aiohttp
8
+ curl_cffi
9
+ techzdl>=1.2.6
10
+ tqdm
11
+ dill
runtime.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ python-3.11
sample.env ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Required Vars
2
+
3
+ API_ID=123456
4
+ API_HASH=dagsjdhgjfsahgjfh
5
+ BOT_TOKENS=21413535:gkdshajfhjfakhjf
6
+ STORAGE_CHANNEL=-100123456789
7
+ DATABASE_BACKUP_MSG_ID=123
start_main.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ import os
2
+
3
+ os.system("uvicorn main:app --reload")
utils/bot_mode.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from pyrogram import Client, filters
3
+ from pyrogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton
4
+ import config
5
+ from utils.logger import Logger
6
+ from pathlib import Path
7
+
8
+ logger = Logger(__name__)
9
+
10
+ START_CMD = """🚀 **Welcome To TG Drive's Bot Mode**
11
+
12
+ You can use this bot to upload files to your TG Drive website directly instead of doing it from website.
13
+
14
+ 🗄 **Commands:**
15
+ /set_folder - Set folder for file uploads
16
+ /current_folder - Check current folder
17
+
18
+ 📤 **How To Upload Files:** Send a file to this bot and it will be uploaded to your TG Drive website. You can also set a folder for file uploads using /set_folder command.
19
+
20
+ Read more about [TG Drive's Bot Mode](https://github.com/TechShreyash/TGDrive#tg-drives-bot-mode)
21
+ """
22
+
23
+ SET_FOLDER_PATH_CACHE = {} # Cache to store folder path for each folder id
24
+ DRIVE_DATA = None
25
+ BOT_MODE = None
26
+
27
+ session_cache_path = Path(f"./cache")
28
+ session_cache_path.parent.mkdir(parents=True, exist_ok=True)
29
+
30
+ main_bot = Client(
31
+ name="main_bot",
32
+ api_id=config.API_ID,
33
+ api_hash=config.API_HASH,
34
+ bot_token=config.MAIN_BOT_TOKEN,
35
+ sleep_threshold=config.SLEEP_THRESHOLD,
36
+ workdir=session_cache_path,
37
+ )
38
+
39
+
40
+ @main_bot.on_message(
41
+ filters.command(["start", "help"])
42
+ & filters.private
43
+ & filters.user(config.TELEGRAM_ADMIN_IDS),
44
+ )
45
+ async def start_handler(client: Client, message: Message):
46
+ await message.reply_text(START_CMD)
47
+
48
+
49
+ @main_bot.on_message(
50
+ filters.command("set_folder")
51
+ & filters.private
52
+ & filters.user(config.TELEGRAM_ADMIN_IDS),
53
+ )
54
+ async def set_folder_handler(client: Client, message: Message):
55
+ global SET_FOLDER_PATH_CACHE, DRIVE_DATA
56
+
57
+ while True:
58
+ try:
59
+ folder_name = await message.ask(
60
+ "Send the folder name where you want to upload files\n\n/cancel to cancel",
61
+ timeout=60,
62
+ filters=filters.text,
63
+ )
64
+ except asyncio.TimeoutError:
65
+ await message.reply_text("Timeout\n\nUse /set_folder to set folder again")
66
+ return
67
+
68
+ if folder_name.text.lower() == "/cancel":
69
+ await message.reply_text("Cancelled")
70
+ return
71
+
72
+ folder_name = folder_name.text.strip()
73
+ search_result = DRIVE_DATA.search_file_folder(folder_name)
74
+
75
+ # Get folders from search result
76
+ folders = {}
77
+ for item in search_result.values():
78
+ if item.type == "folder":
79
+ folders[item.id] = item
80
+
81
+ if len(folders) == 0:
82
+ await message.reply_text(f"No Folder found with name {folder_name}")
83
+ else:
84
+ break
85
+
86
+ buttons = []
87
+ folder_cache = {}
88
+ folder_cache_id = len(SET_FOLDER_PATH_CACHE) + 1
89
+
90
+ for folder in search_result.values():
91
+ path = folder.path.strip("/")
92
+ folder_path = "/" + ("/" + path + "/" + folder.id).strip("/")
93
+ folder_cache[folder.id] = (folder_path, folder.name)
94
+ buttons.append(
95
+ [
96
+ InlineKeyboardButton(
97
+ folder.name,
98
+ callback_data=f"set_folder_{folder_cache_id}_{folder.id}",
99
+ )
100
+ ]
101
+ )
102
+ SET_FOLDER_PATH_CACHE[folder_cache_id] = folder_cache
103
+
104
+ await message.reply_text(
105
+ "Select the folder where you want to upload files",
106
+ reply_markup=InlineKeyboardMarkup(buttons),
107
+ )
108
+
109
+
110
+ @main_bot.on_callback_query(
111
+ filters.user(config.TELEGRAM_ADMIN_IDS) & filters.regex(r"set_folder_")
112
+ )
113
+ async def set_folder_callback(client: Client, callback_query: Message):
114
+ global SET_FOLDER_PATH_CACHE, BOT_MODE
115
+
116
+ folder_cache_id, folder_id = callback_query.data.split("_")[2:]
117
+
118
+ folder_path_cache = SET_FOLDER_PATH_CACHE.get(int(folder_cache_id))
119
+ if folder_path_cache is None:
120
+ await callback_query.answer("Request Expired, Send /set_folder again")
121
+ await callback_query.message.delete()
122
+ return
123
+
124
+ folder_path, name = folder_path_cache.get(folder_id)
125
+ del SET_FOLDER_PATH_CACHE[int(folder_cache_id)]
126
+ BOT_MODE.set_folder(folder_path, name)
127
+
128
+ await callback_query.answer(f"Folder Set Successfully To : {name}")
129
+ await callback_query.message.edit(
130
+ f"Folder Set Successfully To : {name}\n\nNow you can send / forward files to me and it will be uploaded to this folder."
131
+ )
132
+
133
+
134
+ @main_bot.on_message(
135
+ filters.command("current_folder")
136
+ & filters.private
137
+ & filters.user(config.TELEGRAM_ADMIN_IDS),
138
+ )
139
+ async def current_folder_handler(client: Client, message: Message):
140
+ global BOT_MODE
141
+
142
+ await message.reply_text(f"Current Folder: {BOT_MODE.current_folder_name}")
143
+
144
+
145
+ # Handling when any file is sent to the bot
146
+ @main_bot.on_message(
147
+ filters.private
148
+ & filters.user(config.TELEGRAM_ADMIN_IDS)
149
+ & (
150
+ filters.document
151
+ | filters.video
152
+ | filters.audio
153
+ | filters.photo
154
+ | filters.sticker
155
+ )
156
+ )
157
+ async def file_handler(client: Client, message: Message):
158
+ global BOT_MODE, DRIVE_DATA
159
+
160
+ copied_message = await message.copy(config.STORAGE_CHANNEL)
161
+ file = (
162
+ copied_message.document
163
+ or copied_message.video
164
+ or copied_message.audio
165
+ or copied_message.photo
166
+ or copied_message.sticker
167
+ )
168
+
169
+ DRIVE_DATA.new_file(
170
+ BOT_MODE.current_folder,
171
+ file.file_name,
172
+ copied_message.id,
173
+ file.file_size,
174
+ )
175
+
176
+ await message.reply_text(
177
+ f"""✅ File Uploaded Successfully To Your TG Drive Website
178
+
179
+ **File Name:** {file.file_name}
180
+ **Folder:** {BOT_MODE.current_folder_name}
181
+ """
182
+ )
183
+
184
+
185
+ async def start_bot_mode(d, b):
186
+ global DRIVE_DATA, BOT_MODE
187
+ DRIVE_DATA = d
188
+ BOT_MODE = b
189
+
190
+ logger.info("Starting Main Bot")
191
+ await main_bot.start()
192
+
193
+ await main_bot.send_message(
194
+ config.STORAGE_CHANNEL, "Main Bot Started -> TG Drive's Bot Mode Enabled"
195
+ )
196
+ logger.info("Main Bot Started")
197
+ logger.info("TG Drive's Bot Mode Enabled")
utils/clients.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio, config
2
+ from pathlib import Path
3
+ from pyrogram import Client
4
+ from utils.directoryHandler import backup_drive_data, loadDriveData
5
+ from utils.logger import Logger
6
+ import os
7
+ import signal
8
+
9
+ logger = Logger(__name__)
10
+
11
+ multi_clients = {}
12
+ premium_clients = {}
13
+ work_loads = {}
14
+ premium_work_loads = {}
15
+ main_bot = None
16
+
17
+
18
+ async def initialize_clients():
19
+ global multi_clients, work_loads, premium_clients, premium_work_loads
20
+ logger.info("Initializing Clients")
21
+
22
+ session_cache_path = Path(f"./cache")
23
+ session_cache_path.parent.mkdir(parents=True, exist_ok=True)
24
+
25
+ all_tokens = dict((i, t) for i, t in enumerate(config.BOT_TOKENS, start=1))
26
+ all_sessions = dict(
27
+ (i, s) for i, s in enumerate(config.STRING_SESSIONS, start=len(all_tokens) + 1)
28
+ )
29
+
30
+ async def start_client(client_id, token, type):
31
+ try:
32
+ logger.info(f"Starting - {type.title()} Client {client_id}")
33
+
34
+ if type == "bot":
35
+ client = Client(
36
+ name=str(client_id),
37
+ api_id=config.API_ID,
38
+ api_hash=config.API_HASH,
39
+ bot_token=token,
40
+ workdir=session_cache_path,
41
+ )
42
+ client.loop = asyncio.get_running_loop()
43
+ await client.start()
44
+ await client.send_message(
45
+ config.STORAGE_CHANNEL,
46
+ f"Started - {type.title()} Client {client_id}",
47
+ )
48
+ multi_clients[client_id] = client
49
+ work_loads[client_id] = 0
50
+ elif type == "user":
51
+ client = await Client(
52
+ name=str(client_id),
53
+ api_id=config.API_ID,
54
+ api_hash=config.API_HASH,
55
+ session_string=token,
56
+ sleep_threshold=config.SLEEP_THRESHOLD,
57
+ workdir=session_cache_path,
58
+ no_updates=True,
59
+ ).start()
60
+ await client.send_message(
61
+ config.STORAGE_CHANNEL,
62
+ f"Started - {type.title()} Client {client_id}",
63
+ )
64
+ premium_clients[client_id] = client
65
+ premium_work_loads[client_id] = 0
66
+
67
+ logger.info(f"Started - {type.title()} Client {client_id}")
68
+ except Exception as e:
69
+ logger.error(
70
+ f"Failed To Start {type.title()} Client - {client_id} Error: {e}"
71
+ )
72
+
73
+ await asyncio.gather(
74
+ *(
75
+ [
76
+ start_client(client_id, client, "bot")
77
+ for client_id, client in all_tokens.items()
78
+ ]
79
+ + [
80
+ start_client(client_id, client, "user")
81
+ for client_id, client in all_sessions.items()
82
+ ]
83
+ )
84
+ )
85
+ if len(multi_clients) == 0:
86
+ logger.error("No Clients Were Initialized")
87
+
88
+ # Forcefully terminates the program immediately
89
+ os.kill(os.getpid(), signal.SIGKILL)
90
+
91
+ if len(premium_clients) == 0:
92
+ logger.info("No Premium Clients Were Initialized")
93
+
94
+ logger.info("Clients Initialized")
95
+
96
+ # Load the drive data
97
+ await loadDriveData()
98
+
99
+ # Start the backup drive data task
100
+ asyncio.create_task(backup_drive_data())
101
+
102
+
103
+ def get_client(premium_required=False) -> Client:
104
+ global multi_clients, work_loads, premium_clients, premium_work_loads
105
+
106
+ if premium_required:
107
+ index = min(premium_work_loads, key=premium_work_loads.get)
108
+ premium_work_loads[index] += 1
109
+ return premium_clients[index]
110
+
111
+ index = min(work_loads, key=work_loads.get)
112
+ work_loads[index] += 1
113
+ return multi_clients[index]
utils/directoryHandler.py ADDED
@@ -0,0 +1,387 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import sys
3
+ import config, dill
4
+ from pyrogram.types import InputMediaDocument, Message
5
+ import os, random, string, asyncio
6
+ from utils.logger import Logger
7
+ from datetime import datetime, timezone
8
+ import os
9
+ import signal
10
+
11
+ logger = Logger(__name__)
12
+
13
+ cache_dir = Path("./cache")
14
+ cache_dir.mkdir(parents=True, exist_ok=True)
15
+ drive_cache_path = cache_dir / "drive.data"
16
+
17
+
18
+ def getRandomID():
19
+ global DRIVE_DATA
20
+ while True:
21
+ id = "".join(random.choices(string.ascii_uppercase + string.digits, k=6))
22
+ if not DRIVE_DATA:
23
+ return id
24
+ if id not in DRIVE_DATA.used_ids:
25
+ DRIVE_DATA.used_ids.append(id)
26
+ return id
27
+
28
+
29
+ def get_current_utc_time():
30
+ return datetime.now(timezone.utc).strftime("Date - %Y-%m-%d | Time - %H:%M:%S")
31
+
32
+
33
+ class Folder:
34
+ def __init__(self, name: str, path: str) -> None:
35
+ self.name = name
36
+ self.contents = {}
37
+ if name == "/":
38
+ self.id = "root"
39
+ else:
40
+ self.id = getRandomID()
41
+ self.type = "folder"
42
+ self.trash = False
43
+ self.path = ("/" + path.strip("/") + "/").replace("//", "/")
44
+ self.upload_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
45
+ self.auth_hashes = []
46
+
47
+
48
+ class File:
49
+ def __init__(
50
+ self,
51
+ name: str,
52
+ file_id: int,
53
+ size: int,
54
+ path: str,
55
+ ) -> None:
56
+ self.name = name
57
+ self.file_id = file_id
58
+ self.id = getRandomID()
59
+ self.size = size
60
+ self.type = "file"
61
+ self.trash = False
62
+ self.path = path[:-1] if path[-1] == "/" else path
63
+ self.upload_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
64
+
65
+
66
+ class NewDriveData:
67
+ def __init__(self, contents: dict, used_ids: list) -> None:
68
+ self.contents = contents
69
+ self.used_ids = used_ids
70
+ self.isUpdated = False
71
+
72
+ def save(self) -> None:
73
+ with open(drive_cache_path, "wb") as f:
74
+ dill.dump(self, f)
75
+ self.isUpdated = True
76
+ logger.info("Drive data saved successfully.")
77
+
78
+ def new_folder(self, path: str, name: str) -> None:
79
+ logger.info(f"Creating new folder '{name}' in path '{path}'.")
80
+
81
+ folder = Folder(name, path)
82
+ if path == "/":
83
+ directory_folder: Folder = self.contents[path]
84
+ directory_folder.contents[folder.id] = folder
85
+ else:
86
+ paths = path.strip("/").split("/")
87
+ directory_folder: Folder = self.contents["/"]
88
+ for path in paths:
89
+ directory_folder = directory_folder.contents[path]
90
+ directory_folder.contents[folder.id] = folder
91
+
92
+ self.save()
93
+ return folder.path + folder.id
94
+
95
+ def new_file(self, path: str, name: str, file_id: int, size: int) -> None:
96
+ logger.info(f"Creating new file '{name}' in path '{path}'.")
97
+
98
+ file = File(name, file_id, size, path)
99
+ if path == "/":
100
+ directory_folder: Folder = self.contents[path]
101
+ directory_folder.contents[file.id] = file
102
+ else:
103
+ paths = path.strip("/").split("/")
104
+ directory_folder: Folder = self.contents["/"]
105
+ for path in paths:
106
+ directory_folder = directory_folder.contents[path]
107
+ directory_folder.contents[file.id] = file
108
+
109
+ self.save()
110
+
111
+ def get_directory(
112
+ self, path: str, is_admin: bool = True, auth: str = None
113
+ ) -> Folder:
114
+ folder_data: Folder = self.contents["/"]
115
+ auth_success = False
116
+ auth_home_path = None
117
+
118
+ if path != "/":
119
+ path = path.strip("/")
120
+
121
+ if "/" in path:
122
+ path = path.split("/")
123
+ else:
124
+ path = [path]
125
+
126
+ for folder in path:
127
+ folder_data = folder_data.contents[folder]
128
+
129
+ if auth in folder_data.auth_hashes:
130
+ auth_success = True
131
+ auth_home_path = (
132
+ "/" + folder_data.path.strip("/") + "/" + folder_data.id
133
+ )
134
+
135
+ if not is_admin and not auth_success:
136
+ logger.warning(f"Unauthorized access attempt to path '{path}'.")
137
+ return None
138
+
139
+ if auth_success:
140
+ logger.info(f"Authorization successful for path '{path}'.")
141
+ return folder_data, auth_home_path
142
+
143
+ return folder_data
144
+
145
+ def get_folder_auth(self, path: str) -> None:
146
+ auth = getRandomID()
147
+ folder_data: Folder = self.contents["/"]
148
+
149
+ if path != "/":
150
+ path = path.strip("/")
151
+
152
+ if "/" in path:
153
+ path = path.split("/")
154
+ else:
155
+ path = [path]
156
+
157
+ for folder in path:
158
+ folder_data = folder_data.contents[folder]
159
+
160
+ folder_data.auth_hashes.append(auth)
161
+ self.save()
162
+ logger.info(f"Authorization hash generated for path '{path}'.")
163
+ return auth
164
+
165
+ def get_file(self, path) -> File:
166
+ if len(path.strip("/").split("/")) > 0:
167
+ folder_path = "/" + "/".join(path.strip("/").split("/")[:-1])
168
+ file_id = path.strip("/").split("/")[-1]
169
+ else:
170
+ folder_path = "/"
171
+ file_id = path.strip("/")
172
+
173
+ folder_data = self.get_directory(folder_path)
174
+ return folder_data.contents[file_id]
175
+
176
+ def rename_file_folder(self, path: str, new_name: str) -> None:
177
+ if len(path.strip("/").split("/")) > 0:
178
+ folder_path = "/" + "/".join(path.strip("/").split("/")[:-1])
179
+ file_id = path.strip("/").split("/")[-1]
180
+ else:
181
+ folder_path = "/"
182
+ file_id = path.strip("/")
183
+ folder_data = self.get_directory(folder_path)
184
+ folder_data.contents[file_id].name = new_name
185
+ self.save()
186
+ logger.info(f"Item at path '{path}' renamed to '{new_name}'.")
187
+
188
+ def trash_file_folder(self, path: str, trash: bool) -> None:
189
+ action = "Trashing" if trash else "Restoring"
190
+
191
+ if len(path.strip("/").split("/")) > 0:
192
+ folder_path = "/" + "/".join(path.strip("/").split("/")[:-1])
193
+ file_id = path.strip("/").split("/")[-1]
194
+ else:
195
+ folder_path = "/"
196
+ file_id = path.strip("/")
197
+ folder_data = self.get_directory(folder_path)
198
+ folder_data.contents[file_id].trash = trash
199
+ self.save()
200
+ logger.info(f"Item at path '{path}' {action.lower()} successfully.")
201
+
202
+ def get_trashed_files_folders(self):
203
+ root_dir = self.get_directory("/")
204
+ trash_data = {}
205
+
206
+ def traverse_directory(folder):
207
+ for item in folder.contents.values():
208
+ if item.type == "folder":
209
+ if item.trash:
210
+ trash_data[item.id] = item
211
+ else:
212
+ # Recursively traverse the subfolder
213
+ traverse_directory(item)
214
+ elif item.type == "file":
215
+ if item.trash:
216
+ trash_data[item.id] = item
217
+
218
+ traverse_directory(root_dir)
219
+ return trash_data
220
+
221
+ def delete_file_folder(self, path: str) -> None:
222
+
223
+ if len(path.strip("/").split("/")) > 0:
224
+ folder_path = "/" + "/".join(path.strip("/").split("/")[:-1])
225
+ file_id = path.strip("/").split("/")[-1]
226
+ else:
227
+ folder_path = "/"
228
+ file_id = path.strip("/")
229
+
230
+ folder_data = self.get_directory(folder_path)
231
+ del folder_data.contents[file_id]
232
+ self.save()
233
+ logger.info(f"Item at path '{path}' deleted successfully.")
234
+
235
+ def search_file_folder(self, query: str):
236
+ logger.info(f"Searching for items matching query '{query}'.")
237
+
238
+ root_dir = self.get_directory("/")
239
+ search_results = {}
240
+
241
+ def traverse_directory(folder):
242
+ for item in folder.contents.values():
243
+ if query.lower() in item.name.lower():
244
+ search_results[item.id] = item
245
+ if item.type == "folder":
246
+ traverse_directory(item)
247
+
248
+ traverse_directory(root_dir)
249
+ logger.info(f"Search completed. Found {len(search_results)} matching items.")
250
+ return search_results
251
+
252
+
253
+ class NewBotMode:
254
+ def __init__(self, drive_data: NewDriveData) -> None:
255
+ self.drive_data = drive_data
256
+
257
+ # Set the current folder to root directory by default
258
+ self.current_folder = "/"
259
+ self.current_folder_name = "/ (root directory)"
260
+
261
+ def set_folder(self, folder_path: str, name: str) -> None:
262
+ self.current_folder = folder_path
263
+ self.current_folder_name = name
264
+ self.drive_data.save()
265
+ logger.info(f"Current folder set to '{name}' at path '{folder_path}'.")
266
+
267
+
268
+ DRIVE_DATA: NewDriveData = None
269
+ BOT_MODE: NewBotMode = None
270
+
271
+
272
+ # Function to backup the drive data to telegram
273
+ async def backup_drive_data(loop=True):
274
+ global DRIVE_DATA
275
+ logger.info("Starting backup drive data task.")
276
+
277
+ while True:
278
+ try:
279
+ if not DRIVE_DATA.isUpdated:
280
+ if not loop:
281
+ break
282
+ await asyncio.sleep(config.DATABASE_BACKUP_TIME)
283
+ continue
284
+
285
+ logger.info("Backing up drive data to Telegram.")
286
+ from utils.clients import get_client
287
+
288
+ client = get_client()
289
+ time_text = f"📅 **Last Updated :** {get_current_utc_time()} (UTC +00:00)"
290
+ caption = (
291
+ f"🔐 **TG Drive Data Backup File**\n\n"
292
+ "Do not edit or delete this message. This is a backup file for the tg drive data.\n\n"
293
+ f"{time_text}"
294
+ )
295
+
296
+ media_doc = InputMediaDocument(drive_cache_path, caption=caption)
297
+ msg = await client.edit_message_media(
298
+ config.STORAGE_CHANNEL,
299
+ config.DATABASE_BACKUP_MSG_ID,
300
+ media=media_doc,
301
+ file_name="drive.data",
302
+ )
303
+
304
+ DRIVE_DATA.isUpdated = False
305
+ logger.info("Drive data backed up to Telegram successfully.")
306
+
307
+ try:
308
+ await msg.pin()
309
+ except Exception as pin_e:
310
+ logger.error(f"Error pinning backup message: {pin_e}")
311
+
312
+ if not loop:
313
+ break
314
+
315
+ await asyncio.sleep(config.DATABASE_BACKUP_TIME)
316
+ except Exception as e:
317
+ logger.error(f"Backup Error: {e}")
318
+ await asyncio.sleep(10)
319
+
320
+
321
+ async def init_drive_data():
322
+ global DRIVE_DATA
323
+
324
+ logger.info("Initializing drive data.")
325
+ root_dir = DRIVE_DATA.get_directory("/")
326
+ if not hasattr(root_dir, "auth_hashes"):
327
+ root_dir.auth_hashes = []
328
+
329
+ def traverse_directory(folder):
330
+ for item in folder.contents.values():
331
+ if item.type == "folder":
332
+ traverse_directory(item)
333
+
334
+ if not hasattr(item, "auth_hashes"):
335
+ item.auth_hashes = []
336
+
337
+ traverse_directory(root_dir)
338
+ DRIVE_DATA.save()
339
+ logger.info("Drive data initialization completed.")
340
+
341
+
342
+ async def loadDriveData():
343
+ global DRIVE_DATA, BOT_MODE
344
+
345
+ logger.info("Loading drive data.")
346
+ from utils.clients import get_client
347
+
348
+ client = get_client()
349
+ try:
350
+ try:
351
+ msg: Message = await client.get_messages(
352
+ config.STORAGE_CHANNEL, config.DATABASE_BACKUP_MSG_ID
353
+ )
354
+ except Exception as e:
355
+ logger.error(f"Error fetching backup message: {e}")
356
+
357
+ # Forcefully terminates the program immediately
358
+ os.kill(os.getpid(), signal.SIGKILL)
359
+
360
+ if not msg.document:
361
+ logger.error(f"Error fetching backup message: {e}")
362
+
363
+ # Forcefully terminates the program immediately
364
+ os.kill(os.getpid(), signal.SIGKILL)
365
+
366
+ if msg.document.file_name == "drive.data":
367
+ dl_path = await msg.download()
368
+ with open(dl_path, "rb") as f:
369
+ DRIVE_DATA = dill.load(f)
370
+
371
+ logger.info("Drive data loaded from Telegram backup.")
372
+ else:
373
+ raise Exception("Backup drive.data file not found on Telegram.")
374
+ except Exception as e:
375
+ logger.warning(f"Backup load failed: {e}")
376
+ logger.info("Creating new drive.data file.")
377
+ DRIVE_DATA = NewDriveData({"/": Folder("/", "/")}, [])
378
+ DRIVE_DATA.save()
379
+
380
+ await init_drive_data()
381
+
382
+ if config.MAIN_BOT_TOKEN:
383
+ from utils.bot_mode import start_bot_mode
384
+
385
+ BOT_MODE = NewBotMode(DRIVE_DATA)
386
+ await start_bot_mode(DRIVE_DATA, BOT_MODE)
387
+ logger.info("Bot mode started.")
utils/downloader.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import aiohttp, asyncio
3
+ from utils.extra import get_filename
4
+ from utils.logger import Logger
5
+ from pathlib import Path
6
+ from utils.uploader import start_file_uploader
7
+ from techzdl import TechZDL
8
+
9
+ logger = Logger(__name__)
10
+
11
+ DOWNLOAD_PROGRESS = {}
12
+ STOP_DOWNLOAD = []
13
+
14
+ cache_dir = Path("./cache")
15
+ cache_dir.mkdir(parents=True, exist_ok=True)
16
+
17
+
18
+ async def download_progress_callback(status, current, total, id):
19
+ global DOWNLOAD_PROGRESS
20
+
21
+ DOWNLOAD_PROGRESS[id] = (
22
+ status,
23
+ current,
24
+ total,
25
+ )
26
+
27
+
28
+ async def download_file(url, id, path, filename, singleThreaded):
29
+ global DOWNLOAD_PROGRESS, STOP_DOWNLOAD
30
+
31
+ logger.info(f"Downloading file from {url}")
32
+
33
+ try:
34
+ downloader = TechZDL(
35
+ url,
36
+ output_dir=cache_dir,
37
+ debug=False,
38
+ progress_callback=download_progress_callback,
39
+ progress_args=(id,),
40
+ max_retries=5,
41
+ single_threaded=singleThreaded,
42
+ )
43
+ await downloader.start(in_background=True)
44
+
45
+ await asyncio.sleep(5)
46
+
47
+ while downloader.is_running:
48
+ if id in STOP_DOWNLOAD:
49
+ logger.info(f"Stopping download {id}")
50
+ await downloader.stop()
51
+ return
52
+ await asyncio.sleep(1)
53
+
54
+ if downloader.download_success is False:
55
+ raise downloader.download_error
56
+
57
+ DOWNLOAD_PROGRESS[id] = (
58
+ "completed",
59
+ downloader.total_size,
60
+ downloader.total_size,
61
+ )
62
+
63
+ logger.info(f"File downloaded to {downloader.output_path}")
64
+
65
+ asyncio.create_task(
66
+ start_file_uploader(
67
+ downloader.output_path, id, path, filename, downloader.total_size
68
+ )
69
+ )
70
+ except Exception as e:
71
+ DOWNLOAD_PROGRESS[id] = ("error", 0, 0)
72
+ logger.error(f"Failed to download file: {url} {e}")
73
+
74
+
75
+ async def get_file_info_from_url(url):
76
+ downloader = TechZDL(
77
+ url,
78
+ output_dir=cache_dir,
79
+ debug=False,
80
+ progress_callback=download_progress_callback,
81
+ progress_args=(id,),
82
+ max_retries=5,
83
+ )
84
+ file_info = await downloader.get_file_info()
85
+ return {"file_size": file_info["total_size"], "file_name": file_info["filename"]}
utils/extra.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import mimetypes
2
+ from urllib.parse import unquote_plus
3
+ import re
4
+ import urllib.parse
5
+ from pathlib import Path
6
+ from config import WEBSITE_URL
7
+ import asyncio, aiohttp
8
+ from utils.directoryHandler import get_current_utc_time, getRandomID
9
+ from utils.logger import Logger
10
+
11
+ logger = Logger(__name__)
12
+
13
+
14
+ def convert_class_to_dict(data, isObject, showtrash=False):
15
+ if isObject == True:
16
+ data = data.__dict__.copy()
17
+ new_data = {"contents": {}}
18
+
19
+ for key in data["contents"]:
20
+ if data["contents"][key].trash == showtrash:
21
+ if data["contents"][key].type == "folder":
22
+ folder = data["contents"][key]
23
+ new_data["contents"][key] = {
24
+ "name": folder.name,
25
+ "type": folder.type,
26
+ "id": folder.id,
27
+ "path": folder.path,
28
+ "upload_date": folder.upload_date,
29
+ }
30
+ else:
31
+ file = data["contents"][key]
32
+ new_data["contents"][key] = {
33
+ "name": file.name,
34
+ "type": file.type,
35
+ "size": file.size,
36
+ "id": file.id,
37
+ "path": file.path,
38
+ "upload_date": file.upload_date,
39
+ }
40
+ return new_data
41
+
42
+
43
+ async def auto_ping_website():
44
+ if WEBSITE_URL is not None:
45
+ async with aiohttp.ClientSession() as session:
46
+ while True:
47
+ try:
48
+ async with session.get(WEBSITE_URL) as response:
49
+ if response.status == 200:
50
+ logger.info(f"Pinged website at {get_current_utc_time()}")
51
+ else:
52
+ logger.warning(f"Failed to ping website: {response.status}")
53
+ except Exception as e:
54
+ logger.warning(f"Failed to ping website: {e}")
55
+
56
+ await asyncio.sleep(60) # Ping website every minute
57
+
58
+
59
+ import shutil
60
+
61
+
62
+ def reset_cache_dir():
63
+ cache_dir = Path("./cache")
64
+ downloads_dir = Path("./downloads")
65
+ shutil.rmtree(cache_dir, ignore_errors=True)
66
+ shutil.rmtree(downloads_dir, ignore_errors=True)
67
+ cache_dir.mkdir(parents=True, exist_ok=True)
68
+ downloads_dir.mkdir(parents=True, exist_ok=True)
69
+ logger.info("Cache and downloads directory reset")
70
+
71
+
72
+ def parse_content_disposition(content_disposition):
73
+ # Split the content disposition into parts
74
+ parts = content_disposition.split(";")
75
+
76
+ # Initialize filename variable
77
+ filename = None
78
+
79
+ # Loop through parts to find the filename
80
+ for part in parts:
81
+ part = part.strip()
82
+ if part.startswith("filename="):
83
+ # If filename is found
84
+ filename = part.split("=", 1)[1]
85
+ elif part.startswith("filename*="):
86
+ # If filename* is found
87
+ match = re.match(r"filename\*=(\S*)''(.*)", part)
88
+ if match:
89
+ encoding, value = match.groups()
90
+ try:
91
+ filename = urllib.parse.unquote(value, encoding=encoding)
92
+ except ValueError:
93
+ # Handle invalid encoding
94
+ pass
95
+
96
+ if filename is None:
97
+ raise Exception("Failed to get filename")
98
+ return filename
99
+
100
+
101
+ def get_filename(headers, url):
102
+ try:
103
+ if headers.get("Content-Disposition"):
104
+ filename = parse_content_disposition(headers["Content-Disposition"])
105
+ else:
106
+ filename = unquote_plus(url.strip("/").split("/")[-1])
107
+
108
+ filename = filename.strip(' "')
109
+ except:
110
+ filename = unquote_plus(url.strip("/").split("/")[-1])
111
+
112
+ filename = filename.strip()
113
+
114
+ if filename == "" or "." not in filename:
115
+ if headers.get("Content-Type"):
116
+ extension = mimetypes.guess_extension(headers["Content-Type"])
117
+ if extension:
118
+ filename = f"{getRandomID()}{extension}"
119
+ else:
120
+ filename = getRandomID()
121
+ else:
122
+ filename = getRandomID()
123
+
124
+ return filename
utils/logger.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from tqdm import tqdm
3
+
4
+
5
+ class TqdmLoggingHandler(logging.Handler):
6
+ def emit(self, record):
7
+ try:
8
+ msg = self.format(record)
9
+ tqdm.write(msg)
10
+ self.flush()
11
+ except Exception:
12
+ self.handleError(record)
13
+
14
+
15
+ class Logger:
16
+ def __init__(self, name, level=logging.DEBUG):
17
+ self.logger = logging.getLogger(name)
18
+ self.logger.setLevel(level)
19
+ self.formatter = logging.Formatter("%(name)s - %(levelname)s - %(message)s")
20
+
21
+ # Remove existing handlers to prevent duplicate logs
22
+ if self.logger.hasHandlers():
23
+ self.logger.handlers.clear()
24
+
25
+ # FileHandler for logging to a file
26
+ file_handler = logging.FileHandler("logs.txt", mode="w")
27
+ file_handler.setFormatter(self.formatter)
28
+ self.logger.addHandler(file_handler)
29
+
30
+ # Custom TqdmLoggingHandler for console output
31
+ stream_handler = TqdmLoggingHandler()
32
+ stream_handler.setFormatter(self.formatter)
33
+ self.logger.addHandler(stream_handler)
34
+
35
+ def debug(self, message):
36
+ self.logger.debug(message)
37
+
38
+ def info(self, message):
39
+ self.logger.info(message)
40
+
41
+ def warning(self, message):
42
+ self.logger.warning(message)
43
+
44
+ def error(self, message):
45
+ self.logger.error(message)
46
+
47
+ def critical(self, message):
48
+ self.logger.critical(message)
utils/streamer/__init__.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math, mimetypes
2
+ from fastapi.responses import StreamingResponse, Response
3
+ from utils.logger import Logger
4
+ from utils.streamer.custom_dl import ByteStreamer
5
+ from utils.streamer.file_properties import get_name
6
+ from utils.clients import (
7
+ get_client,
8
+ )
9
+ from urllib.parse import quote
10
+
11
+ logger = Logger(__name__)
12
+
13
+ class_cache = {}
14
+
15
+
16
+ async def media_streamer(channel: int, message_id: int, file_name: str, request):
17
+ global class_cache
18
+
19
+ range_header = request.headers.get("Range", 0)
20
+
21
+ faster_client = get_client()
22
+
23
+ if faster_client in class_cache:
24
+ tg_connect = class_cache[faster_client]
25
+ else:
26
+ tg_connect = ByteStreamer(faster_client)
27
+ class_cache[faster_client] = tg_connect
28
+
29
+ file_id = await tg_connect.get_file_properties(channel, message_id)
30
+ file_size = file_id.file_size
31
+
32
+ if range_header:
33
+ from_bytes, until_bytes = range_header.replace("bytes=", "").split("-")
34
+ from_bytes = int(from_bytes)
35
+ until_bytes = int(until_bytes) if until_bytes else file_size - 1
36
+ else:
37
+ from_bytes = 0
38
+ until_bytes = file_size - 1
39
+
40
+ if (until_bytes > file_size) or (from_bytes < 0) or (until_bytes < from_bytes):
41
+ return Response(
42
+ status_code=416,
43
+ content="416: Range not satisfiable",
44
+ headers={"Content-Range": f"bytes */{file_size}"},
45
+ )
46
+
47
+ chunk_size = 1024 * 1024
48
+ until_bytes = min(until_bytes, file_size - 1)
49
+
50
+ offset = from_bytes - (from_bytes % chunk_size)
51
+ first_part_cut = from_bytes - offset
52
+ last_part_cut = until_bytes % chunk_size + 1
53
+
54
+ req_length = until_bytes - from_bytes + 1
55
+ part_count = math.ceil(until_bytes / chunk_size) - math.floor(offset / chunk_size)
56
+ body = tg_connect.yield_file(
57
+ file_id, offset, first_part_cut, last_part_cut, part_count, chunk_size
58
+ )
59
+
60
+ disposition = "attachment"
61
+ mime_type = mimetypes.guess_type(file_name.lower())[0] or "application/octet-stream"
62
+
63
+ if (
64
+ "video/" in mime_type
65
+ or "audio/" in mime_type
66
+ or "image/" in mime_type
67
+ or "/html" in mime_type
68
+ ):
69
+ disposition = "inline"
70
+
71
+ return StreamingResponse(
72
+ status_code=206 if range_header else 200,
73
+ content=body,
74
+ headers={
75
+ "Content-Type": f"{mime_type}",
76
+ "Content-Range": f"bytes {from_bytes}-{until_bytes}/{file_size}",
77
+ "Content-Length": str(req_length),
78
+ "Content-Disposition": f'{disposition}; filename="{quote(file_name)}"',
79
+ "Accept-Ranges": "bytes",
80
+ },
81
+ media_type=mime_type,
82
+ )
utils/streamer/custom_dl.py ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from typing import Dict, Union
3
+ from pyrogram import Client, utils, raw
4
+ from .file_properties import get_file_ids
5
+ from pyrogram.session import Session, Auth
6
+ from pyrogram.errors import AuthBytesInvalid
7
+ from pyrogram.file_id import FileId, FileType, ThumbnailSource
8
+ from utils.logger import Logger
9
+
10
+ logger = Logger(__name__)
11
+
12
+
13
+ class ByteStreamer:
14
+ def __init__(self, client: Client):
15
+ self.clean_timer = 30 * 60
16
+ self.client: Client = client
17
+ self.cached_file_ids: Dict[int, FileId] = {}
18
+ asyncio.create_task(self.clean_cache())
19
+
20
+ async def get_file_properties(self, channel, message_id: int) -> FileId:
21
+ if message_id not in self.cached_file_ids:
22
+ await self.generate_file_properties(channel, message_id)
23
+ return self.cached_file_ids[message_id]
24
+
25
+ async def generate_file_properties(self, channel, message_id: int) -> FileId:
26
+ file_id = await get_file_ids(self.client, channel, message_id)
27
+ if not file_id:
28
+ raise Exception("FileNotFound")
29
+ self.cached_file_ids[message_id] = file_id
30
+ return self.cached_file_ids[message_id]
31
+
32
+ async def generate_media_session(self, client: Client, file_id: FileId) -> Session:
33
+ """
34
+ Generates the media session for the DC that contains the media file.
35
+ This is required for getting the bytes from Telegram servers.
36
+ """
37
+
38
+ media_session = client.media_sessions.get(file_id.dc_id, None)
39
+
40
+ if media_session is None:
41
+ if file_id.dc_id != await client.storage.dc_id():
42
+ media_session = Session(
43
+ client,
44
+ file_id.dc_id,
45
+ await Auth(
46
+ client, file_id.dc_id, await client.storage.test_mode()
47
+ ).create(),
48
+ await client.storage.test_mode(),
49
+ is_media=True,
50
+ )
51
+ await media_session.start()
52
+
53
+ for _ in range(6):
54
+ exported_auth = await client.invoke(
55
+ raw.functions.auth.ExportAuthorization(dc_id=file_id.dc_id)
56
+ )
57
+
58
+ try:
59
+ await media_session.invoke(
60
+ raw.functions.auth.ImportAuthorization(
61
+ id=exported_auth.id, bytes=exported_auth.bytes
62
+ )
63
+ )
64
+ break
65
+ except AuthBytesInvalid:
66
+ logger.debug(
67
+ f"Invalid authorization bytes for DC {file_id.dc_id}"
68
+ )
69
+ continue
70
+ else:
71
+ await media_session.stop()
72
+ raise AuthBytesInvalid
73
+ else:
74
+ media_session = Session(
75
+ client,
76
+ file_id.dc_id,
77
+ await client.storage.auth_key(),
78
+ await client.storage.test_mode(),
79
+ is_media=True,
80
+ )
81
+ await media_session.start()
82
+ logger.debug(f"Created media session for DC {file_id.dc_id}")
83
+ client.media_sessions[file_id.dc_id] = media_session
84
+ else:
85
+ logger.debug(f"Using cached media session for DC {file_id.dc_id}")
86
+ return media_session
87
+
88
+ @staticmethod
89
+ async def get_location(
90
+ file_id: FileId,
91
+ ) -> Union[
92
+ raw.types.InputPhotoFileLocation,
93
+ raw.types.InputDocumentFileLocation,
94
+ raw.types.InputPeerPhotoFileLocation,
95
+ ]:
96
+ """
97
+ Returns the file location for the media file.
98
+ """
99
+ file_type = file_id.file_type
100
+
101
+ if file_type == FileType.CHAT_PHOTO:
102
+ if file_id.chat_id > 0:
103
+ peer = raw.types.InputPeerUser(
104
+ user_id=file_id.chat_id, access_hash=file_id.chat_access_hash
105
+ )
106
+ else:
107
+ if file_id.chat_access_hash == 0:
108
+ peer = raw.types.InputPeerChat(chat_id=-file_id.chat_id)
109
+ else:
110
+ peer = raw.types.InputPeerChannel(
111
+ channel_id=utils.get_channel_id(file_id.chat_id),
112
+ access_hash=file_id.chat_access_hash,
113
+ )
114
+
115
+ location = raw.types.InputPeerPhotoFileLocation(
116
+ peer=peer,
117
+ volume_id=file_id.volume_id,
118
+ local_id=file_id.local_id,
119
+ big=file_id.thumbnail_source == ThumbnailSource.CHAT_PHOTO_BIG,
120
+ )
121
+ elif file_type == FileType.PHOTO:
122
+ location = raw.types.InputPhotoFileLocation(
123
+ id=file_id.media_id,
124
+ access_hash=file_id.access_hash,
125
+ file_reference=file_id.file_reference,
126
+ thumb_size=file_id.thumbnail_size,
127
+ )
128
+ else:
129
+ location = raw.types.InputDocumentFileLocation(
130
+ id=file_id.media_id,
131
+ access_hash=file_id.access_hash,
132
+ file_reference=file_id.file_reference,
133
+ thumb_size=file_id.thumbnail_size,
134
+ )
135
+ return location
136
+
137
+ async def yield_file(
138
+ self,
139
+ file_id: FileId,
140
+ offset: int,
141
+ first_part_cut: int,
142
+ last_part_cut: int,
143
+ part_count: int,
144
+ chunk_size: int,
145
+ ):
146
+ """
147
+ Custom generator that yields the bytes of the media file.
148
+ """
149
+ client = self.client
150
+ logger.debug(f"Starting to yielding file with client.")
151
+ media_session = await self.generate_media_session(client, file_id)
152
+
153
+ current_part = 1
154
+ location = await self.get_location(file_id)
155
+
156
+ try:
157
+ r = await media_session.invoke(
158
+ raw.functions.upload.GetFile(
159
+ location=location, offset=offset, limit=chunk_size
160
+ ),
161
+ )
162
+ if isinstance(r, raw.types.upload.File):
163
+ while True:
164
+ chunk = r.bytes
165
+ if not chunk:
166
+ break
167
+ elif part_count == 1:
168
+ yield chunk[first_part_cut:last_part_cut]
169
+ elif current_part == 1:
170
+ yield chunk[first_part_cut:]
171
+ elif current_part == part_count:
172
+ yield chunk[:last_part_cut]
173
+ else:
174
+ yield chunk
175
+
176
+ current_part += 1
177
+ offset += chunk_size
178
+
179
+ if current_part > part_count:
180
+ break
181
+
182
+ r = await media_session.invoke(
183
+ raw.functions.upload.GetFile(
184
+ location=location, offset=offset, limit=chunk_size
185
+ ),
186
+ )
187
+ except (TimeoutError, AttributeError):
188
+ pass
189
+ finally:
190
+ logger.debug(f"Finished yielding file with {current_part} parts.")
191
+
192
+ async def clean_cache(self) -> None:
193
+ """
194
+ function to clean the cache to reduce memory usage
195
+ """
196
+ while True:
197
+ await asyncio.sleep(self.clean_timer)
198
+ self.cached_file_ids.clear()
199
+ logger.debug("Cleaned the cache")
utils/streamer/file_properties.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pyrogram import Client
2
+ from pyrogram.types import Message
3
+ from pyrogram.file_id import FileId
4
+ from typing import Any, Optional, Union
5
+ from pyrogram.raw.types.messages import Messages
6
+ from datetime import datetime
7
+
8
+
9
+ async def parse_file_id(message: "Message") -> Optional[FileId]:
10
+ media = get_media_from_message(message)
11
+ if media:
12
+ return FileId.decode(media.file_id)
13
+
14
+
15
+ async def parse_file_unique_id(message: "Messages") -> Optional[str]:
16
+ media = get_media_from_message(message)
17
+ if media:
18
+ return media.file_unique_id
19
+
20
+
21
+ async def get_file_ids(client: Client, chat_id, message_id) -> Optional[FileId]:
22
+ message = await client.get_messages(chat_id, int(message_id))
23
+ if message.empty:
24
+ raise Exception("FileNotFound")
25
+ media = get_media_from_message(message)
26
+ file_unique_id = await parse_file_unique_id(message)
27
+ file_id = await parse_file_id(message)
28
+ setattr(file_id, "file_size", getattr(media, "file_size", 0))
29
+ setattr(file_id, "mime_type", getattr(media, "mime_type", ""))
30
+ setattr(file_id, "file_name", getattr(media, "file_name", ""))
31
+ setattr(file_id, "unique_id", file_unique_id)
32
+ return file_id
33
+
34
+
35
+ def get_media_from_message(message: "Message") -> Any:
36
+ media_types = (
37
+ "audio",
38
+ "document",
39
+ "photo",
40
+ "sticker",
41
+ "animation",
42
+ "video",
43
+ "voice",
44
+ "video_note",
45
+ )
46
+ for attr in media_types:
47
+ media = getattr(message, attr, None)
48
+ if media:
49
+ return media
50
+
51
+
52
+ def get_name(media_msg: Union[Message, FileId]) -> str:
53
+ if isinstance(media_msg, Message):
54
+ media = get_media_from_message(media_msg)
55
+ file_name = getattr(media, "file_name", "")
56
+
57
+ elif isinstance(media_msg, FileId):
58
+ file_name = getattr(media_msg, "file_name", "")
59
+
60
+ if not file_name:
61
+ if isinstance(media_msg, Message) and media_msg.media:
62
+ media_type = media_msg.media.value
63
+ elif media_msg.file_type:
64
+ media_type = media_msg.file_type.name.lower()
65
+ else:
66
+ media_type = "file"
67
+
68
+ formats = {
69
+ "photo": "jpg",
70
+ "audio": "mp3",
71
+ "voice": "ogg",
72
+ "video": "mp4",
73
+ "animation": "mp4",
74
+ "video_note": "mp4",
75
+ "sticker": "webp",
76
+ }
77
+
78
+ ext = formats.get(media_type)
79
+ ext = "." + ext if ext else ""
80
+
81
+ date = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
82
+ file_name = f"{media_type}-{date}{ext}"
83
+
84
+ return file_name
utils/uploader.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from utils.clients import get_client
2
+ from pyrogram import Client
3
+ from pyrogram.types import Message
4
+ from config import STORAGE_CHANNEL
5
+ import os
6
+ from utils.logger import Logger
7
+ from urllib.parse import unquote_plus
8
+
9
+ logger = Logger(__name__)
10
+ PROGRESS_CACHE = {}
11
+ STOP_TRANSMISSION = []
12
+
13
+
14
+ async def progress_callback(current, total, id, client: Client, file_path):
15
+ global PROGRESS_CACHE, STOP_TRANSMISSION
16
+
17
+ PROGRESS_CACHE[id] = ("running", current, total)
18
+ if id in STOP_TRANSMISSION:
19
+ logger.info(f"Stopping transmission {id}")
20
+ client.stop_transmission()
21
+ try:
22
+ os.remove(file_path)
23
+ except:
24
+ pass
25
+
26
+
27
+ async def start_file_uploader(
28
+ file_path, id, directory_path, filename, file_size, delete=True
29
+ ):
30
+ global PROGRESS_CACHE
31
+ from utils.directoryHandler import DRIVE_DATA
32
+
33
+ logger.info(f"Uploading file {file_path} {id}")
34
+
35
+ if file_size > 1.98 * 1024 * 1024 * 1024:
36
+ # Use premium client for files larger than 2 GB
37
+ client: Client = get_client(premium_required=True)
38
+ else:
39
+ client: Client = get_client()
40
+
41
+ PROGRESS_CACHE[id] = ("running", 0, 0)
42
+
43
+ message: Message = await client.send_document(
44
+ STORAGE_CHANNEL,
45
+ file_path,
46
+ progress=progress_callback,
47
+ progress_args=(id, client, file_path),
48
+ disable_notification=True,
49
+ )
50
+ size = (
51
+ message.photo
52
+ or message.document
53
+ or message.video
54
+ or message.audio
55
+ or message.sticker
56
+ ).file_size
57
+
58
+ filename = unquote_plus(filename)
59
+
60
+ DRIVE_DATA.new_file(directory_path, filename, message.id, size)
61
+ PROGRESS_CACHE[id] = ("completed", size, size)
62
+
63
+ logger.info(f"Uploaded file {file_path} {id}")
64
+
65
+ if delete:
66
+ try:
67
+ os.remove(file_path)
68
+ except Exception as e:
69
+ pass
website/VideoPlayer.html ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>TG Drive - Video Player</title>
8
+
9
+ <link href="//vjs.zencdn.net/8.3.0/video-js.min.css" rel="stylesheet">
10
+ <script src="//vjs.zencdn.net/8.3.0/video.min.js"></script>
11
+ <style>
12
+ body {
13
+ display: flex;
14
+ flex-direction: column;
15
+ align-items: center;
16
+ justify-content: center;
17
+ height: 100vh;
18
+ margin: 0;
19
+ font-family: Arial, sans-serif;
20
+ }
21
+
22
+ .video-container {
23
+ width: 90%;
24
+ max-width: 800px;
25
+ }
26
+
27
+ .buttons-container {
28
+ margin-top: 20px;
29
+ display: flex;
30
+ justify-content: space-evenly;
31
+ width: 90%;
32
+ max-width: 800px;
33
+ }
34
+
35
+ .copy-button {
36
+ padding: 10px 20px;
37
+ font-size: 16px;
38
+ cursor: pointer;
39
+ background-color: #007bff;
40
+ color: white;
41
+ border: none;
42
+ border-radius: 5px;
43
+ transition: background-color 0.3s ease;
44
+ }
45
+
46
+ .copy-button:hover {
47
+ background-color: #0056b3;
48
+ }
49
+ </style>
50
+ </head>
51
+
52
+ <body>
53
+
54
+ <div class="video-container">
55
+ <video id="my-player" class="video-js vjs-fluid" controls preload="auto" data-setup='{}'>
56
+ <source id="video-src" src="" type="video/mp4">
57
+ </source>
58
+ <p class="vjs-no-js">
59
+ To view this video please enable JavaScript, and consider upgrading to a
60
+ web browser that
61
+ <a href="https://videojs.com/html5-video-support/" target="_blank">
62
+ supports HTML5 video
63
+ </a>
64
+ </p>
65
+ </video>
66
+ </div>
67
+
68
+ <div class="buttons-container">
69
+ <button class="copy-button" onclick="copyStreamUrl()">Copy Stream URL</button>
70
+ <button class="copy-button" onclick="copyDownloadUrl()">Copy Download URL</button>
71
+ </div>
72
+
73
+ <script>
74
+ const downloadUrl = (new URL(window.location.href)).searchParams.get('url');
75
+ document.getElementById('video-src').src = downloadUrl;
76
+
77
+
78
+ function copyTextToClipboard(text) {
79
+ if (navigator.clipboard && navigator.clipboard.writeText) {
80
+ navigator.clipboard.writeText(text).then(function () {
81
+ alert('Link copied to clipboard!');
82
+ }).catch(function (err) {
83
+ console.error('Could not copy text: ', err);
84
+ fallbackCopyTextToClipboard(text);
85
+ });
86
+ } else {
87
+ fallbackCopyTextToClipboard(text);
88
+ }
89
+ }
90
+
91
+ function fallbackCopyTextToClipboard(text) {
92
+ const textArea = document.createElement('textarea');
93
+ textArea.value = text;
94
+ document.body.appendChild(textArea);
95
+ textArea.focus();
96
+ textArea.select();
97
+
98
+ try {
99
+ const successful = document.execCommand('copy');
100
+ if (successful) {
101
+ alert('Link copied to clipboard!');
102
+ } else {
103
+ alert('Failed to copy the link.');
104
+ }
105
+ } catch (err) {
106
+ console.error('Fallback: Oops, unable to copy', err);
107
+ }
108
+
109
+ document.body.removeChild(textArea);
110
+ }
111
+
112
+ function copyStreamUrl() {
113
+ copyTextToClipboard(window.location.href);
114
+ }
115
+
116
+ function copyDownloadUrl() {
117
+ copyTextToClipboard(downloadUrl);
118
+ }
119
+ </script>
120
+
121
+ </body>
122
+
123
+ </html>
website/home.html ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>TG Drive</title>
8
+ <link rel="stylesheet" href="static/home.css" />
9
+
10
+ <!-- Fonts Start -->
11
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
12
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
13
+ <link
14
+ href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"
15
+ rel="stylesheet" />
16
+ <!-- Fonts End -->
17
+ </head>
18
+
19
+ <body>
20
+ <div class="container">
21
+ <!-- Sidebar Start -->
22
+ <div class="sidebar">
23
+ <div class="sidebar-header">
24
+ <img src="https://ssl.gstatic.com/images/branding/product/1x/drive_2020q4_48dp.png" />
25
+ <span>TG Drive</span>
26
+ </div>
27
+
28
+ <button id="new-button" class="new-button">
29
+ <img src="static/assets/plus-icon.svg" />New
30
+ </button>
31
+
32
+ <div id="new-upload" class="new-upload">
33
+ <input id="new-upload-focus" type="text"
34
+ style="height: 0px; width: 0px; border: none; position: absolute" readonly />
35
+ <div id="new-folder-btn">
36
+ <img src="static/assets/folder-icon.svg" />
37
+ New Folder
38
+ </div>
39
+ <hr />
40
+ <div id="file-upload-btn">
41
+ <img src="static/assets/upload-icon.svg" />
42
+ File Upload
43
+ </div>
44
+ <input type="file" id="fileInput" style="height: 0px; width: 0px; border: none; position: absolute" />
45
+ <hr />
46
+ <div id="url-upload-btn">
47
+ <img src="static/assets/link-icon.svg" />
48
+ URL Upload
49
+ </div>
50
+ </div>
51
+
52
+ <div class="sidebar-menu">
53
+ <a class="selected-item" href="/?path=/"><img src="static/assets/home-icon.svg" />Home</a>
54
+ <a class="unselected-item" href="/?path=/trash"><img src="static/assets/trash-icon.svg" />Trash</a>
55
+ </div>
56
+ </div>
57
+ <!-- Sidebar End -->
58
+
59
+ <div id="bg-blur" class="bg-blur"></div>
60
+
61
+ <!-- Create New Folder Start -->
62
+ <div id="create-new-folder" class="create-new-folder">
63
+ <span>New Folder</span>
64
+ <input type="text" id="new-folder-name" placeholder="Enter Folder Name" autocomplete="off" />
65
+ <div>
66
+ <button id="new-folder-cancel">Cancel</button>
67
+ <button id="new-folder-create">Create</button>
68
+ </div>
69
+ </div>
70
+ <!-- Create New Folder End -->
71
+
72
+ <!-- File / Folder Rename Start -->
73
+ <div id="rename-file-folder" class="create-new-folder">
74
+ <span>Edit File/Folder Name</span>
75
+ <input type="text" id="rename-name" placeholder="Enter File/Folder Name" autocomplete="off" />
76
+ <div>
77
+ <button id="rename-cancel">Cancel</button>
78
+ <button id="rename-create">Rename</button>
79
+ </div>
80
+ </div>
81
+ <!-- File / Folder Rename End -->
82
+
83
+ <!-- Remote Upload Start -->
84
+ <div id="new-url-upload" class="create-new-folder">
85
+ <span>Url Upload</span>
86
+ <input type="text" id="remote-url" placeholder="Enter Direct Download Link Of File" autocomplete="off" />
87
+ <div id="single-threaded-div">
88
+ <input type="checkbox" name="single-threaded-toggle" id="single-threaded-toggle">
89
+ <label for="single-threaded-toggle">Single Threaded</label>
90
+ <a href="#"><img src="static/assets/info-icon-small.svg" alt="Info"></a>
91
+ </div>
92
+ <div>
93
+ <button id="remote-cancel">Cancel</button>
94
+ <button id="remote-start">Upload</button>
95
+ </div>
96
+ </div>
97
+ <!-- Remote Upload End -->
98
+
99
+ <!-- Get Password Start -->
100
+ <div id="get-password" class="create-new-folder">
101
+ <span>Admin Login</span>
102
+ <input type="text" id="auth-pass" placeholder="Enter Password" autocomplete="off" />
103
+ <div>
104
+ <button id="pass-login">Login</button>
105
+ </div>
106
+ </div>
107
+ <!-- Get Password End -->
108
+
109
+ <!-- File Uploader Start -->
110
+ <div id="file-uploader" class="file-uploader">
111
+ <span class="upload-head">🚀 Uploading File...</span>
112
+ <span id="upload-filename" class="upload-info">Filename : </span>
113
+ <span id="upload-filesize" class="upload-info">Filesize :</span>
114
+ <span id="upload-status" class="upload-info">Status : </span>
115
+ <span id="upload-percent" class="upload-info">Progress : </span>
116
+ <div class="progress">
117
+ <div class="progress-bar" id="progress-bar"></div>
118
+ </div>
119
+ <div class="btn-div">
120
+ <button id="cancel-file-upload">Cancel Upload</button>
121
+ </div>
122
+ </div>
123
+ <!-- File Uploader End -->
124
+
125
+ <!-- Main Content Start -->
126
+ <div class="main-content">
127
+ <div class="header">
128
+ <div class="search-bar">
129
+ <img src="static/assets/search-icon.svg" />
130
+ <form id="search-form">
131
+ <input id="file-search" type="text" placeholder="Search in Drive" autocomplete="off" />
132
+ </form>
133
+
134
+ </div>
135
+ </div>
136
+
137
+ <div class="directory">
138
+ <table>
139
+ <thead>
140
+ <tr>
141
+ <th>Name</th>
142
+ <th>File Size</th>
143
+ <th>More</th>
144
+ </tr>
145
+ </thead>
146
+ <tbody id="directory-data"></tbody>
147
+ </table>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ <script src="static/js/extra.js"></script>
152
+ <script src="static/js/apiHandler.js"></script>
153
+ <script src="static/js/sidebar.js"></script>
154
+ <script src="static/js/fileClickHandler.js"></script>
155
+ <script src="static/js/main.js"></script>
156
+ </body>
157
+
158
+ </html>
website/static/assets/file-icon.svg ADDED
website/static/assets/folder-icon.svg ADDED
website/static/assets/folder-solid-icon.svg ADDED
website/static/assets/home-icon.svg ADDED
website/static/assets/info-icon-small.svg ADDED
website/static/assets/link-icon.svg ADDED
website/static/assets/load-icon.svg ADDED
website/static/assets/more-icon.svg ADDED
website/static/assets/pencil-icon.svg ADDED
website/static/assets/plus-icon.svg ADDED
website/static/assets/profile-icon.svg ADDED
website/static/assets/search-icon.svg ADDED
website/static/assets/share-icon.svg ADDED
website/static/assets/trash-icon.svg ADDED
website/static/assets/upload-icon.svg ADDED
website/static/home.css ADDED
@@ -0,0 +1,527 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0px;
3
+ padding: 0px;
4
+ box-sizing: border-box;
5
+ font-family: "Roboto", sans-serif;
6
+ user-select: none;
7
+ -webkit-user-select: none;
8
+ -moz-user-select: none;
9
+ -ms-user-select: none;
10
+ }
11
+
12
+ .container {
13
+ width: 100%;
14
+ height: 100vh;
15
+ display: grid;
16
+ background: #f1f1f1;
17
+ grid-template-columns: auto 1fr;
18
+ }
19
+
20
+ .rotate-90 {
21
+ transform: rotate(90deg);
22
+ }
23
+
24
+ /* Sidebar Style Start */
25
+
26
+ .sidebar {
27
+ width: 250px;
28
+ height: 100%;
29
+ background-color: #f8fafd;
30
+ padding: 20px;
31
+ display: flex;
32
+ flex-direction: column;
33
+ justify-content: start;
34
+ align-items: start;
35
+ }
36
+
37
+ .sidebar-header {
38
+ display: flex;
39
+ align-items: center;
40
+ justify-content: start;
41
+ }
42
+
43
+ .sidebar-header img {
44
+ width: 40px;
45
+ height: 40px;
46
+ margin-right: 10px;
47
+ }
48
+
49
+ .sidebar-header span {
50
+ font-size: 1.2rem;
51
+ font-weight: 500;
52
+ color: #444746;
53
+ }
54
+
55
+ .sidebar .new-button {
56
+ padding: 15px 20px;
57
+ background-color: #fff;
58
+ border: 1px solid #e0e0e0;
59
+ border-radius: 20px;
60
+ margin-top: 20px;
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: center;
64
+ cursor: pointer;
65
+ transition: all 0.3s ease;
66
+ box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
67
+ font-size: 0.9rem;
68
+ color: #282c35;
69
+ font-weight: 500;
70
+ }
71
+
72
+ .sidebar .new-button:hover {
73
+ background-color: #edf1fa;
74
+ border: 1px solid #c9d0e6;
75
+ box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.3);
76
+ }
77
+
78
+ .sidebar .new-button img {
79
+ width: 24px;
80
+ height: 24px;
81
+ margin-right: 10px;
82
+ }
83
+
84
+ .new-upload {
85
+ position: absolute;
86
+ background-color: #ffffff;
87
+ padding: 5px 0px;
88
+ box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2);
89
+ border-radius: 5px;
90
+ width: 250px;
91
+ top: 40px;
92
+ z-index: -1;
93
+ opacity: 0;
94
+ transition: all 0.2s;
95
+ }
96
+
97
+ .new-upload div {
98
+ padding: 10px 20px;
99
+ display: flex;
100
+ align-items: center;
101
+ justify-content: start;
102
+ cursor: pointer;
103
+ transition: all 0.3s ease;
104
+ font-size: 14px;
105
+ }
106
+
107
+ .new-upload div:hover {
108
+ background-color: #f1f1f1;
109
+ }
110
+
111
+ .new-upload div img {
112
+ margin-right: 10px;
113
+ height: 20px;
114
+ width: 20px;
115
+ }
116
+
117
+ .new-upload hr {
118
+ border: 1px solid #e0e0e0;
119
+ margin: 5px 0px;
120
+ }
121
+
122
+ .bg-blur {
123
+ position: fixed;
124
+ background-color: #000000;
125
+ height: 100dvh;
126
+ width: 100dvw;
127
+ transition: opacity 0.3s ease;
128
+ z-index: -1;
129
+ opacity: 0;
130
+ }
131
+
132
+ /* More Options Start */
133
+
134
+ .more-options {
135
+ position: absolute;
136
+ background-color: #ffffff;
137
+ padding: 2px 0px;
138
+ box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2);
139
+ border-radius: 5px;
140
+ z-index: -1;
141
+ opacity: 0;
142
+ transition: all 0.2s;
143
+ width: 150px;
144
+ transform: translateX(-50%);
145
+ }
146
+
147
+ .more-options div {
148
+ width: 100%;
149
+ padding: 5px 20px;
150
+ display: flex;
151
+ align-items: center;
152
+ justify-content: center;
153
+ cursor: pointer;
154
+ transition: all 0.3s ease;
155
+ font-size: 14px;
156
+ }
157
+
158
+ .more-options div:hover {
159
+ background-color: #f1f1f1;
160
+ }
161
+
162
+ .more-options div img {
163
+ margin-right: 10px;
164
+ height: 20px;
165
+ width: 20px;
166
+ }
167
+
168
+ .more-options hr {
169
+ border: 1px solid #e0e0e0;
170
+ margin: 2px 0px;
171
+ }
172
+
173
+ /* More Options End */
174
+
175
+
176
+ /* Create New Folder Start */
177
+ .create-new-folder {
178
+ position: fixed;
179
+ top: 50%;
180
+ left: 50%;
181
+ transform: translate(-50%, -50%);
182
+ padding: 20px;
183
+ display: flex;
184
+ align-items: center;
185
+ justify-content: center;
186
+ flex-direction: column;
187
+ background-color: #fff;
188
+ border-radius: 10px;
189
+ z-index: -1;
190
+ opacity: 0;
191
+ box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3);
192
+ transition: opacity 0.3s ease;
193
+ width: 400px;
194
+ }
195
+
196
+ .create-new-folder span {
197
+ font-size: 1.2rem;
198
+ font-weight: 400;
199
+ color: #444746;
200
+ margin-bottom: 20px;
201
+ width: 100%;
202
+ margin-left: 10px;
203
+ }
204
+
205
+ .create-new-folder input {
206
+ width: 100%;
207
+ padding: 10px 20px;
208
+ border: 1px solid #e0e0e0;
209
+ border-radius: 5px;
210
+ outline: none;
211
+ font-size: 0.9rem;
212
+ font-weight: 400;
213
+ color: #444746;
214
+ margin-bottom: 20px;
215
+ }
216
+
217
+ .create-new-folder div {
218
+ width: 100%;
219
+ display: flex;
220
+ align-items: center;
221
+ justify-content: end;
222
+ gap: 10px;
223
+ }
224
+
225
+ .create-new-folder button {
226
+ background-color: transparent;
227
+ border: none;
228
+ border-radius: 20px;
229
+ display: flex;
230
+ align-items: center;
231
+ justify-content: center;
232
+ cursor: pointer;
233
+ transition: all 0.3s ease;
234
+ font-size: 0.9rem;
235
+ color: #0b57d0;
236
+ font-weight: 500;
237
+ padding: 10px 20px;
238
+ }
239
+
240
+ .create-new-folder button:hover {
241
+ background-color: #edf1fa;
242
+ box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.2);
243
+ }
244
+
245
+ /* Create New Folder End */
246
+
247
+ /* Remote URL Upload Start */
248
+
249
+ #single-threaded-div{
250
+ display: flex;
251
+ flex-direction: row;
252
+ justify-content: start;
253
+ align-items: center;
254
+ width: 100%;
255
+ margin: 10px 0px;
256
+ }
257
+
258
+ #single-threaded-div input{
259
+ width: auto;
260
+ margin: 0px;
261
+ margin-left: 10px;
262
+ }
263
+
264
+ #single-threaded-div img{
265
+ height: 16px;
266
+ width: 16px;
267
+ }
268
+ #single-threaded-div a{
269
+ height: 16px;
270
+ width: 16px;
271
+ }
272
+
273
+ /* Remote URL Upload End */
274
+
275
+ /* File Uploader Start */
276
+
277
+ .file-uploader {
278
+ position: fixed;
279
+ top: 50%;
280
+ left: 50%;
281
+ transform: translate(-50%, -50%);
282
+ padding: 20px;
283
+ display: flex;
284
+ align-items: center;
285
+ justify-content: center;
286
+ flex-direction: column;
287
+ background-color: #fff;
288
+ border-radius: 10px;
289
+ z-index: -1;
290
+ box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3);
291
+ transition: opacity 0.3s ease;
292
+ width: 500px;
293
+ opacity: 0;
294
+ }
295
+
296
+ .upload-head {
297
+ font-size: 1.2rem;
298
+ font-weight: 500;
299
+ color: #444746;
300
+ margin-bottom: 20px;
301
+ width: 100%;
302
+ }
303
+
304
+ .upload-info {
305
+ font-size: 16px;
306
+ font-weight: 400;
307
+ color: #444746;
308
+ margin-bottom: 5px;
309
+ width: 100%;
310
+ max-height: 40px;
311
+ overflow: hidden;
312
+ }
313
+
314
+ .progress {
315
+ width: 100%;
316
+ background-color: #ddd;
317
+ border-radius: 5px;
318
+ overflow: hidden;
319
+ margin-top: 20px;
320
+ }
321
+
322
+ .progress-bar {
323
+ height: 20px;
324
+ background-color: #007bff;
325
+ width: 0;
326
+ transition: width 0.3s;
327
+ }
328
+
329
+ .file-uploader .btn-div {
330
+ width: 100%;
331
+ display: flex;
332
+ align-items: center;
333
+ justify-content: end;
334
+ margin-top: 20px;
335
+ }
336
+
337
+ .file-uploader button {
338
+ background-color: transparent;
339
+ border: none;
340
+ border-radius: 20px;
341
+ display: flex;
342
+ align-items: center;
343
+ justify-content: center;
344
+ cursor: pointer;
345
+ transition: all 0.3s ease;
346
+ font-size: 0.9rem;
347
+ color: #0b57d0;
348
+ font-weight: 500;
349
+ padding: 10px 20px;
350
+ }
351
+
352
+ .file-uploader button:hover {
353
+ background-color: #edf1fa;
354
+ box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.2);
355
+ }
356
+
357
+ /* File Uploader End */
358
+
359
+ .sidebar-menu {
360
+ margin-top: 20px;
361
+ width: 100%;
362
+ display: flex;
363
+ flex-direction: column;
364
+ justify-content: start;
365
+ align-items: center;
366
+ }
367
+
368
+ .sidebar-menu a {
369
+ width: 100%;
370
+ padding: 10px 20px;
371
+ display: flex;
372
+ align-items: center;
373
+ justify-content: start;
374
+ color: #444746;
375
+ font-size: 0.9rem;
376
+ font-weight: 400;
377
+ text-decoration: none;
378
+ transition: all 0.3s ease;
379
+ border-radius: 20px;
380
+ }
381
+
382
+ .sidebar-menu .selected-item {
383
+ background-color: #c2e7ff;
384
+ }
385
+
386
+ .sidebar-menu .unselected-item:hover {
387
+ background-color: #cfcfcf;
388
+ }
389
+
390
+ .sidebar-menu a img {
391
+ width: 20px;
392
+ height: 20px;
393
+ margin-right: 10px;
394
+ }
395
+
396
+ /* Sidebar Style End */
397
+
398
+ /* Main Content Style Start */
399
+
400
+ .main-content {
401
+ width: 100%;
402
+ height: 100%;
403
+ padding: 20px;
404
+ display: flex;
405
+ flex-direction: column;
406
+ justify-content: start;
407
+ align-items: start;
408
+ background-color: #f8fafd;
409
+ }
410
+
411
+ .main-content .header {
412
+ width: 100%;
413
+ display: grid;
414
+ grid-template-columns: 1fr auto;
415
+ align-items: center;
416
+ gap: 40px;
417
+ }
418
+
419
+ #search-form {
420
+ height: 100%;
421
+ width: 100%;
422
+ }
423
+
424
+ .search-bar {
425
+ width: 100%;
426
+ display: flex;
427
+ align-items: center;
428
+ justify-content: start;
429
+ background-color: #e9eef6;
430
+ border-radius: 20px;
431
+ padding: 10px 20px;
432
+ }
433
+
434
+ .search-bar img {
435
+ width: 20px;
436
+ height: 20px;
437
+ margin-right: 20px;
438
+ }
439
+
440
+ .search-bar input {
441
+ width: 100%;
442
+ border: none;
443
+ outline: none;
444
+ background-color: transparent;
445
+ font-size: 1rem;
446
+ font-weight: 400;
447
+ }
448
+
449
+ .search-bar:focus-within {
450
+ background-color: #ffffff;
451
+ box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
452
+ }
453
+
454
+ .directory {
455
+ background-color: #fff;
456
+ padding: 10px 20px;
457
+ width: 100%;
458
+ height: 100%;
459
+ border-radius: 20px;
460
+ margin-top: 20px;
461
+ }
462
+
463
+ .directory table {
464
+ width: 100%;
465
+ border-collapse: collapse;
466
+ }
467
+
468
+ .directory table tr th {
469
+ text-align: start;
470
+ font-size: 0.9rem;
471
+ font-weight: 500;
472
+ color: #444746;
473
+ padding: 10px 0px;
474
+ border-bottom: 1px solid #e0e0e0;
475
+ }
476
+
477
+ .directory table tr td {
478
+ text-align: start;
479
+ font-size: 0.9rem;
480
+ font-weight: 400;
481
+ color: #444746;
482
+ border-bottom: 1px solid #e0e0e0;
483
+ height: 50px;
484
+ }
485
+
486
+ .directory .body-tr {
487
+ transition: all 0.3s ease;
488
+ }
489
+
490
+ .directory .body-tr:hover {
491
+ background-color: #f1f1f1;
492
+ }
493
+
494
+ .directory .td-align {
495
+ display: flex;
496
+ align-items: center;
497
+ justify-content: start;
498
+ width: 100%;
499
+ height: 100%;
500
+ }
501
+
502
+ .directory .td-align img {
503
+ width: 24px;
504
+ height: 24px;
505
+ margin-right: 10px;
506
+ }
507
+
508
+ .directory .more-btn img {
509
+ width: 14px;
510
+ height: 14px;
511
+ margin: 0px;
512
+ }
513
+
514
+ .directory .more-btn {
515
+ padding: 8px;
516
+ border-radius: 20px;
517
+ display: flex;
518
+ align-items: center;
519
+ justify-content: center;
520
+ cursor: pointer;
521
+ transition: all 0.3s ease;
522
+ }
523
+
524
+ .directory .more-btn:hover {
525
+ background-color: #b9b9b9;
526
+ box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
527
+ }
website/static/js/apiHandler.js ADDED
@@ -0,0 +1,347 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Api Fuctions
2
+ async function postJson(url, data) {
3
+ data['password'] = getPassword()
4
+ const response = await fetch(url, {
5
+ method: 'POST',
6
+ headers: {
7
+ 'Content-Type': 'application/json'
8
+ },
9
+ body: JSON.stringify(data)
10
+ })
11
+ return await response.json()
12
+ }
13
+
14
+ document.getElementById('pass-login').addEventListener('click', async () => {
15
+ const password = document.getElementById('auth-pass').value
16
+ const data = { 'pass': password }
17
+ const json = await postJson('/api/checkPassword', data)
18
+ if (json.status === 'ok') {
19
+ localStorage.setItem('password', password)
20
+ alert('Logged In Successfully')
21
+ window.location.reload()
22
+ }
23
+ else {
24
+ alert('Wrong Password')
25
+ }
26
+
27
+ })
28
+
29
+ async function getCurrentDirectory() {
30
+ let path = getCurrentPath()
31
+ if (path === 'redirect') {
32
+ return
33
+ }
34
+ try {
35
+ const auth = getFolderAuthFromPath()
36
+ console.log(path)
37
+
38
+ const data = { 'path': path, 'auth': auth }
39
+ const json = await postJson('/api/getDirectory', data)
40
+
41
+ if (json.status === 'ok') {
42
+ if (getCurrentPath().startsWith('/share')) {
43
+ const sections = document.querySelector('.sidebar-menu').getElementsByTagName('a')
44
+ console.log(path)
45
+
46
+ if (removeSlash(json['auth_home_path']) === removeSlash(path.split('_')[1])) {
47
+ sections[0].setAttribute('class', 'selected-item')
48
+
49
+ } else {
50
+ sections[0].setAttribute('class', 'unselected-item')
51
+ }
52
+ sections[0].href = `/?path=/share_${removeSlash(json['auth_home_path'])}&auth=${auth}`
53
+ console.log(`/?path=/share_${removeSlash(json['auth_home_path'])}&auth=${auth}`)
54
+ }
55
+
56
+ console.log(json)
57
+ showDirectory(json['data'])
58
+ } else {
59
+ alert('404 Current Directory Not Found')
60
+ }
61
+ }
62
+ catch (err) {
63
+ console.log(err)
64
+ alert('404 Current Directory Not Found')
65
+ }
66
+ }
67
+
68
+ async function createNewFolder() {
69
+ const folderName = document.getElementById('new-folder-name').value;
70
+ const path = getCurrentPath()
71
+ if (path === 'redirect') {
72
+ return
73
+ }
74
+ if (folderName.length > 0) {
75
+ const data = {
76
+ 'name': folderName,
77
+ 'path': path
78
+ }
79
+ try {
80
+ const json = await postJson('/api/createNewFolder', data)
81
+
82
+ if (json.status === 'ok') {
83
+ window.location.reload();
84
+ } else {
85
+ alert(json.status)
86
+ }
87
+ }
88
+ catch (err) {
89
+ alert('Error Creating Folder')
90
+ }
91
+ } else {
92
+ alert('Folder Name Cannot Be Empty')
93
+ }
94
+ }
95
+
96
+
97
+ async function getFolderShareAuth(path) {
98
+ const data = { 'path': path }
99
+ const json = await postJson('/api/getFolderShareAuth', data)
100
+ if (json.status === 'ok') {
101
+ return json.auth
102
+ } else {
103
+ alert('Error Getting Folder Share Auth')
104
+ }
105
+ }
106
+
107
+ // File Uploader Start
108
+
109
+ const MAX_FILE_SIZE = MAX_FILE_SIZE__SDGJDG // Will be replaced by the python
110
+
111
+ const fileInput = document.getElementById('fileInput');
112
+ const progressBar = document.getElementById('progress-bar');
113
+ const cancelButton = document.getElementById('cancel-file-upload');
114
+ const uploadPercent = document.getElementById('upload-percent');
115
+ let uploadRequest = null;
116
+ let uploadStep = 0;
117
+ let uploadID = null;
118
+
119
+ fileInput.addEventListener('change', async (e) => {
120
+ const file = fileInput.files[0];
121
+
122
+ if (file.size > MAX_FILE_SIZE) {
123
+ alert(`File size exceeds ${(MAX_FILE_SIZE / (1024 * 1024 * 1024)).toFixed(2)} GB limit`);
124
+ return;
125
+ }
126
+
127
+ // Showing file uploader
128
+ document.getElementById('bg-blur').style.zIndex = '2';
129
+ document.getElementById('bg-blur').style.opacity = '0.1';
130
+ document.getElementById('file-uploader').style.zIndex = '3';
131
+ document.getElementById('file-uploader').style.opacity = '1';
132
+
133
+ document.getElementById('upload-filename').innerText = 'Filename: ' + file.name;
134
+ document.getElementById('upload-filesize').innerText = 'Filesize: ' + (file.size / (1024 * 1024)).toFixed(2) + ' MB';
135
+ document.getElementById('upload-status').innerText = 'Status: Uploading To Backend Server';
136
+
137
+
138
+ const formData = new FormData();
139
+ formData.append('file', file);
140
+ formData.append('path', getCurrentPath());
141
+ formData.append('password', getPassword());
142
+ const id = getRandomId();
143
+ formData.append('id', id);
144
+ formData.append('total_size', file.size);
145
+
146
+ uploadStep = 1;
147
+ uploadRequest = new XMLHttpRequest();
148
+ uploadRequest.open('POST', '/api/upload', true);
149
+
150
+ uploadRequest.upload.addEventListener('progress', (e) => {
151
+ if (e.lengthComputable) {
152
+ const percentComplete = (e.loaded / e.total) * 100;
153
+ progressBar.style.width = percentComplete + '%';
154
+ uploadPercent.innerText = 'Progress : ' + percentComplete.toFixed(2) + '%';
155
+ }
156
+ });
157
+
158
+ uploadRequest.upload.addEventListener('load', async () => {
159
+ await updateSaveProgress(id)
160
+ });
161
+
162
+ uploadRequest.upload.addEventListener('error', () => {
163
+ alert('Upload failed');
164
+ window.location.reload();
165
+ });
166
+
167
+ uploadRequest.send(formData);
168
+ });
169
+
170
+ cancelButton.addEventListener('click', () => {
171
+ if (uploadStep === 1) {
172
+ uploadRequest.abort();
173
+ } else if (uploadStep === 2) {
174
+ const data = { 'id': uploadID }
175
+ postJson('/api/cancelUpload', data)
176
+ }
177
+ alert('Upload canceled');
178
+ window.location.reload();
179
+ });
180
+
181
+ async function updateSaveProgress(id) {
182
+ console.log('save progress')
183
+ progressBar.style.width = '0%';
184
+ uploadPercent.innerText = 'Progress : 0%'
185
+ document.getElementById('upload-status').innerText = 'Status: Processing File On Backend Server';
186
+
187
+ const interval = setInterval(async () => {
188
+ const response = await postJson('/api/getSaveProgress', { 'id': id })
189
+ const data = response['data']
190
+
191
+ if (data[0] === 'running') {
192
+ const current = data[1];
193
+ const total = data[2];
194
+ document.getElementById('upload-filesize').innerText = 'Filesize: ' + (total / (1024 * 1024)).toFixed(2) + ' MB';
195
+
196
+ const percentComplete = (current / total) * 100;
197
+ progressBar.style.width = percentComplete + '%';
198
+ uploadPercent.innerText = 'Progress : ' + percentComplete.toFixed(2) + '%';
199
+ }
200
+ else if (data[0] === 'completed') {
201
+ clearInterval(interval);
202
+ uploadPercent.innerText = 'Progress : 100%'
203
+ progressBar.style.width = '100%';
204
+
205
+ await handleUpload2(id)
206
+ }
207
+ }, 3000)
208
+
209
+ }
210
+
211
+ async function handleUpload2(id) {
212
+ console.log(id)
213
+ document.getElementById('upload-status').innerText = 'Status: Uploading To Telegram Server';
214
+ progressBar.style.width = '0%';
215
+ uploadPercent.innerText = 'Progress : 0%';
216
+
217
+ const interval = setInterval(async () => {
218
+ const response = await postJson('/api/getUploadProgress', { 'id': id })
219
+ const data = response['data']
220
+
221
+ if (data[0] === 'running') {
222
+ const current = data[1];
223
+ const total = data[2];
224
+ document.getElementById('upload-filesize').innerText = 'Filesize: ' + (total / (1024 * 1024)).toFixed(2) + ' MB';
225
+
226
+ let percentComplete
227
+ if (total === 0) {
228
+ percentComplete = 0
229
+ }
230
+ else {
231
+ percentComplete = (current / total) * 100;
232
+ }
233
+ progressBar.style.width = percentComplete + '%';
234
+ uploadPercent.innerText = 'Progress : ' + percentComplete.toFixed(2) + '%';
235
+ }
236
+ else if (data[0] === 'completed') {
237
+ clearInterval(interval);
238
+ alert('Upload Completed')
239
+ window.location.reload();
240
+ }
241
+ }, 3000)
242
+ }
243
+
244
+ // File Uploader End
245
+
246
+
247
+ // URL Uploader Start
248
+
249
+ async function get_file_info_from_url(url) {
250
+ const data = { 'url': url }
251
+ const json = await postJson('/api/getFileInfoFromUrl', data)
252
+ if (json.status === 'ok') {
253
+ return json.data
254
+ } else {
255
+ throw new Error(`Error Getting File Info : ${json.status}`)
256
+ }
257
+
258
+ }
259
+
260
+ async function start_file_download_from_url(url, filename, singleThreaded) {
261
+ const data = { 'url': url, 'path': getCurrentPath(), 'filename': filename, 'singleThreaded': singleThreaded }
262
+ const json = await postJson('/api/startFileDownloadFromUrl', data)
263
+ if (json.status === 'ok') {
264
+ return json.id
265
+ } else {
266
+ throw new Error(`Error Starting File Download : ${json.status}`)
267
+ }
268
+ }
269
+
270
+ async function download_progress_updater(id, file_name, file_size) {
271
+ uploadID = id;
272
+ uploadStep = 2
273
+ // Showing file uploader
274
+ document.getElementById('bg-blur').style.zIndex = '2';
275
+ document.getElementById('bg-blur').style.opacity = '0.1';
276
+ document.getElementById('file-uploader').style.zIndex = '3';
277
+ document.getElementById('file-uploader').style.opacity = '1';
278
+
279
+ document.getElementById('upload-filename').innerText = 'Filename: ' + file_name;
280
+ document.getElementById('upload-filesize').innerText = 'Filesize: ' + (file_size / (1024 * 1024)).toFixed(2) + ' MB';
281
+
282
+ const interval = setInterval(async () => {
283
+ const response = await postJson('/api/getFileDownloadProgress', { 'id': id })
284
+ const data = response['data']
285
+
286
+ if (data[0] === 'error') {
287
+ clearInterval(interval);
288
+ alert('Failed To Download File From URL To Backend Server')
289
+ window.location.reload()
290
+ }
291
+ else if (data[0] === 'completed') {
292
+ clearInterval(interval);
293
+ uploadPercent.innerText = 'Progress : 100%'
294
+ progressBar.style.width = '100%';
295
+ await handleUpload2(id)
296
+ }
297
+ else {
298
+ const current = data[1];
299
+ const total = data[2];
300
+
301
+ const percentComplete = (current / total) * 100;
302
+ progressBar.style.width = percentComplete + '%';
303
+ uploadPercent.innerText = 'Progress : ' + percentComplete.toFixed(2) + '%';
304
+
305
+ if (data[0] === 'Downloading') {
306
+ document.getElementById('upload-status').innerText = 'Status: Downloading File From Url To Backend Server';
307
+ }
308
+ else {
309
+ document.getElementById('upload-status').innerText = `Status: ${data[0]}`;
310
+ }
311
+ }
312
+ }, 3000)
313
+ }
314
+
315
+
316
+ async function Start_URL_Upload() {
317
+ try {
318
+ document.getElementById('new-url-upload').style.opacity = '0';
319
+ setTimeout(() => {
320
+ document.getElementById('new-url-upload').style.zIndex = '-1';
321
+ }, 300)
322
+
323
+ const file_url = document.getElementById('remote-url').value
324
+ const singleThreaded = document.getElementById('single-threaded-toggle').checked
325
+
326
+ const file_info = await get_file_info_from_url(file_url)
327
+ const file_name = file_info.file_name
328
+ const file_size = file_info.file_size
329
+
330
+ if (file_size > MAX_FILE_SIZE) {
331
+ throw new Error(`File size exceeds ${(MAX_FILE_SIZE / (1024 * 1024 * 1024)).toFixed(2)} GB limit`)
332
+ }
333
+
334
+ const id = await start_file_download_from_url(file_url, file_name, singleThreaded)
335
+
336
+ await download_progress_updater(id, file_name, file_size)
337
+
338
+ }
339
+ catch (err) {
340
+ alert(err)
341
+ window.location.reload()
342
+ }
343
+
344
+
345
+ }
346
+
347
+ // URL Uploader End
website/static/js/extra.js ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function getCurrentPath() {
2
+ const url = new URL(window.location.href);
3
+ const path = url.searchParams.get('path')
4
+ if (path === null) {
5
+ window.location.href = '/?path=/'
6
+ return 'redirect'
7
+ }
8
+ return path
9
+ }
10
+
11
+ function getFolderAuthFromPath() {
12
+ const url = new URL(window.location.href);
13
+ const auth = url.searchParams.get('auth')
14
+ return auth
15
+ }
16
+
17
+ // Changing sidebar section class
18
+ if (getCurrentPath() !== '/') {
19
+ const sections = document.querySelector('.sidebar-menu').getElementsByTagName('a')
20
+ sections[0].setAttribute('class', 'unselected-item')
21
+
22
+ if (getCurrentPath().includes('/trash')) {
23
+ sections[1].setAttribute('class', 'selected-item')
24
+ }
25
+ }
26
+
27
+ function convertBytes(bytes) {
28
+ const kilobyte = 1024;
29
+ const megabyte = kilobyte * 1024;
30
+ const gigabyte = megabyte * 1024;
31
+
32
+ if (bytes >= gigabyte) {
33
+ return (bytes / gigabyte).toFixed(2) + ' GB';
34
+ } else if (bytes >= megabyte) {
35
+ return (bytes / megabyte).toFixed(2) + ' MB';
36
+ } else if (bytes >= kilobyte) {
37
+ return (bytes / kilobyte).toFixed(2) + ' KB';
38
+ } else {
39
+ return bytes + ' bytes';
40
+ }
41
+ }
42
+
43
+ const INPUTS = {}
44
+
45
+ function validateInput(event) {
46
+ console.log('Validating Input')
47
+ const pattern = /^[a-zA-Z0-9 \-_\\[\]()@#!$%*+={}:;<>,.?/|\\~`]*$/;;
48
+ const input = event.target;
49
+ if (!pattern.test(input.value)) {
50
+ input.value = INPUTS[input.id]
51
+ } else {
52
+ INPUTS[input.id] = input.value
53
+ }
54
+ }
55
+
56
+ function getRootUrl() {
57
+ const url = new URL(window.location.href);
58
+ const protocol = url.protocol; // Get the protocol, e.g., "https:"
59
+ const hostname = url.hostname; // Get the hostname, e.g., "sub.example.com" or "192.168.1.1"
60
+ const port = url.port; // Get the port, e.g., "8080"
61
+
62
+ const rootUrl = `${protocol}//${hostname}${port ? ':' + port : ''}`;
63
+
64
+ return rootUrl;
65
+ }
66
+
67
+ function copyTextToClipboard(text) {
68
+ if (navigator.clipboard && navigator.clipboard.writeText) {
69
+ navigator.clipboard.writeText(text).then(function () {
70
+ alert('Link copied to clipboard!');
71
+ }).catch(function (err) {
72
+ console.error('Could not copy text: ', err);
73
+ fallbackCopyTextToClipboard(text);
74
+ });
75
+ } else {
76
+ fallbackCopyTextToClipboard(text);
77
+ }
78
+ }
79
+
80
+ function fallbackCopyTextToClipboard(text) {
81
+ const textArea = document.createElement('textarea');
82
+ textArea.value = text;
83
+ document.body.appendChild(textArea);
84
+ textArea.focus();
85
+ textArea.select();
86
+
87
+ try {
88
+ const successful = document.execCommand('copy');
89
+ if (successful) {
90
+ alert('Link copied to clipboard!');
91
+ } else {
92
+ alert('Failed to copy the link.');
93
+ }
94
+ } catch (err) {
95
+ console.error('Fallback: Oops, unable to copy', err);
96
+ }
97
+
98
+ document.body.removeChild(textArea);
99
+ }
100
+
101
+ function getPassword() {
102
+ return localStorage.getItem('password')
103
+ }
104
+
105
+ function getRandomId() {
106
+ const length = 6;
107
+ const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
108
+ let result = '';
109
+ for (let i = 0; i < length; i++) {
110
+ result += characters.charAt(Math.floor(Math.random() * characters.length));
111
+ }
112
+ return result;
113
+ }
114
+
115
+ function removeSlash(text) {
116
+ let charactersToRemove = "[/]+"; // Define the characters to remove inside square brackets
117
+ let trimmedStr = text.replace(new RegExp(`^${charactersToRemove}|${charactersToRemove}$`, 'g'), '');
118
+ return trimmedStr;
119
+ }
website/static/js/fileClickHandler.js ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function openFolder() {
2
+ let path = (getCurrentPath() + '/' + this.getAttribute('data-id') + '/').replaceAll('//', '/')
3
+
4
+ const auth = getFolderAuthFromPath()
5
+ if (auth) {
6
+ path = path + '&auth=' + auth
7
+ }
8
+ window.location.href = `/?path=${path}`
9
+ }
10
+
11
+ function openFile() {
12
+ const fileName = this.getAttribute('data-name').toLowerCase()
13
+ let path = '/file?path=' + this.getAttribute('data-path') + '/' + this.getAttribute('data-id')
14
+
15
+ if (fileName.endsWith('.mp4') || fileName.endsWith('.mkv') || fileName.endsWith('.webm') || fileName.endsWith('.mov') || fileName.endsWith('.avi') || fileName.endsWith('.ts') || fileName.endsWith('.ogv')) {
16
+ path = '/stream?url=' + getRootUrl() + path
17
+ }
18
+
19
+ window.open(path, '_blank')
20
+ }
21
+
22
+
23
+ // File More Button Handler Start
24
+
25
+ function openMoreButton(div) {
26
+ const id = div.getAttribute('data-id')
27
+ const moreDiv = document.getElementById(`more-option-${id}`)
28
+
29
+ const rect = div.getBoundingClientRect();
30
+ const x = rect.left + window.scrollX - 40;
31
+ const y = rect.top + window.scrollY;
32
+
33
+ moreDiv.style.zIndex = 2
34
+ moreDiv.style.opacity = 1
35
+ moreDiv.style.left = `${x}px`
36
+ moreDiv.style.top = `${y}px`
37
+
38
+ const isTrash = getCurrentPath().includes('/trash')
39
+
40
+ moreDiv.querySelector('.more-options-focus').focus()
41
+ moreDiv.querySelector('.more-options-focus').addEventListener('blur', closeMoreBtnFocus);
42
+ moreDiv.querySelector('.more-options-focus').addEventListener('focusout', closeMoreBtnFocus);
43
+ if (!isTrash) {
44
+ moreDiv.querySelector(`#rename-${id}`).addEventListener('click', renameFileFolder)
45
+ moreDiv.querySelector(`#trash-${id}`).addEventListener('click', trashFileFolder)
46
+ try {
47
+ moreDiv.querySelector(`#share-${id}`).addEventListener('click', shareFile)
48
+ }
49
+ catch { }
50
+ try {
51
+ moreDiv.querySelector(`#folder-share-${id}`).addEventListener('click', shareFolder)
52
+ }
53
+ catch { }
54
+ }
55
+ else {
56
+ moreDiv.querySelector(`#restore-${id}`).addEventListener('click', restoreFileFolder)
57
+ moreDiv.querySelector(`#delete-${id}`).addEventListener('click', deleteFileFolder)
58
+ }
59
+ }
60
+
61
+ function closeMoreBtnFocus() {
62
+ const moreDiv = this.parentElement
63
+ moreDiv.style.opacity = '0'
64
+ setTimeout(() => {
65
+ moreDiv.style.zIndex = '-1'
66
+ }, 300)
67
+ }
68
+
69
+ // Rename File Folder Start
70
+ function renameFileFolder() {
71
+ const id = this.getAttribute('id').split('-')[1]
72
+ console.log(id)
73
+
74
+ document.getElementById('rename-name').value = this.parentElement.getAttribute('data-name');
75
+ document.getElementById('bg-blur').style.zIndex = '2';
76
+ document.getElementById('bg-blur').style.opacity = '0.1';
77
+
78
+ document.getElementById('rename-file-folder').style.zIndex = '3';
79
+ document.getElementById('rename-file-folder').style.opacity = '1';
80
+ document.getElementById('rename-file-folder').setAttribute('data-id', id);
81
+ setTimeout(() => {
82
+ document.getElementById('rename-name').focus();
83
+ }, 300)
84
+ }
85
+
86
+ document.getElementById('rename-cancel').addEventListener('click', () => {
87
+ document.getElementById('rename-name').value = '';
88
+ document.getElementById('bg-blur').style.opacity = '0';
89
+ setTimeout(() => {
90
+ document.getElementById('bg-blur').style.zIndex = '-1';
91
+ }, 300)
92
+ document.getElementById('rename-file-folder').style.opacity = '0';
93
+ setTimeout(() => {
94
+ document.getElementById('rename-file-folder').style.zIndex = '-1';
95
+ }, 300)
96
+ });
97
+
98
+ document.getElementById('rename-create').addEventListener('click', async () => {
99
+ const name = document.getElementById('rename-name').value;
100
+ if (name === '') {
101
+ alert('Name cannot be empty')
102
+ return
103
+ }
104
+
105
+ const id = document.getElementById('rename-file-folder').getAttribute('data-id')
106
+
107
+ const path = document.getElementById(`more-option-${id}`).getAttribute('data-path') + '/' + id
108
+
109
+ const data = {
110
+ 'name': name,
111
+ 'path': path
112
+ }
113
+
114
+ const response = await postJson('/api/renameFileFolder', data)
115
+ if (response.status === 'ok') {
116
+ alert('File/Folder Renamed Successfully')
117
+ window.location.reload();
118
+ } else {
119
+ alert('Failed to rename file/folder')
120
+ window.location.reload();
121
+ }
122
+ });
123
+
124
+
125
+ // Rename File Folder End
126
+
127
+ async function trashFileFolder() {
128
+ const id = this.getAttribute('id').split('-')[1]
129
+ console.log(id)
130
+ const path = document.getElementById(`more-option-${id}`).getAttribute('data-path') + '/' + id
131
+ const data = {
132
+ 'path': path,
133
+ 'trash': true
134
+ }
135
+ const response = await postJson('/api/trashFileFolder', data)
136
+
137
+ if (response.status === 'ok') {
138
+ alert('File/Folder Sent to Trash Successfully')
139
+ window.location.reload();
140
+ } else {
141
+ alert('Failed to Send File/Folder to Trash')
142
+ window.location.reload();
143
+ }
144
+ }
145
+
146
+ async function restoreFileFolder() {
147
+ const id = this.getAttribute('id').split('-')[1]
148
+ const path = this.getAttribute('data-path') + '/' + id
149
+ const data = {
150
+ 'path': path,
151
+ 'trash': false
152
+ }
153
+ const response = await postJson('/api/trashFileFolder', data)
154
+
155
+ if (response.status === 'ok') {
156
+ alert('File/Folder Restored Successfully')
157
+ window.location.reload();
158
+ } else {
159
+ alert('Failed to Restored File/Folder')
160
+ window.location.reload();
161
+ }
162
+ }
163
+
164
+ async function deleteFileFolder() {
165
+ const id = this.getAttribute('id').split('-')[1]
166
+ const path = this.getAttribute('data-path') + '/' + id
167
+ const data = {
168
+ 'path': path
169
+ }
170
+ const response = await postJson('/api/deleteFileFolder', data)
171
+
172
+ if (response.status === 'ok') {
173
+ alert('File/Folder Deleted Successfully')
174
+ window.location.reload();
175
+ } else {
176
+ alert('Failed to Delete File/Folder')
177
+ window.location.reload();
178
+ }
179
+ }
180
+
181
+ async function shareFile() {
182
+ const fileName = this.parentElement.getAttribute('data-name').toLowerCase()
183
+ const id = this.getAttribute('id').split('-')[1]
184
+ const path = document.getElementById(`more-option-${id}`).getAttribute('data-path') + '/' + id
185
+ const root_url = getRootUrl()
186
+
187
+ let link
188
+ if (fileName.endsWith('.mp4') || fileName.endsWith('.mkv') || fileName.endsWith('.webm') || fileName.endsWith('.mov') || fileName.endsWith('.avi') || fileName.endsWith('.ts') || fileName.endsWith('.ogv')) {
189
+ link = `${root_url}/stream?url=${root_url}/file?path=${path}`
190
+ } else {
191
+ link = `${root_url}/file?path=${path}`
192
+
193
+ }
194
+
195
+ copyTextToClipboard(link)
196
+ }
197
+
198
+
199
+ async function shareFolder() {
200
+ const id = this.getAttribute('id').split('-')[2]
201
+ console.log(id)
202
+ let path = document.getElementById(`more-option-${id}`).getAttribute('data-path') + '/' + id
203
+ const root_url = getRootUrl()
204
+
205
+ const auth = await getFolderShareAuth(path)
206
+ path = path.slice(1)
207
+
208
+ let link = `${root_url}/?path=/share_${path}&auth=${auth}`
209
+ console.log(link)
210
+
211
+ copyTextToClipboard(link)
212
+ }
213
+
214
+ // File More Button Handler End
website/static/js/main.js ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function showDirectory(data) {
2
+ data = data['contents']
3
+ document.getElementById('directory-data').innerHTML = ''
4
+ const isTrash = getCurrentPath().startsWith('/trash')
5
+
6
+ let html = ''
7
+
8
+ // Step 2: Sort the array based on the 'date' values
9
+ let entries = Object.entries(data);
10
+ let folders = entries.filter(([key, value]) => value.type === 'folder');
11
+ let files = entries.filter(([key, value]) => value.type === 'file');
12
+
13
+ folders.sort((a, b) => new Date(b[1].upload_date) - new Date(a[1].upload_date));
14
+ files.sort((a, b) => new Date(b[1].upload_date) - new Date(a[1].upload_date));
15
+
16
+ for (const [key, item] of folders) {
17
+ if (item.type === 'folder') {
18
+ html += `<tr data-path="${item.path}" data-id="${item.id}" class="body-tr folder-tr"><td><div class="td-align"><img src="static/assets/folder-solid-icon.svg">${item.name}</div></td><td><div class="td-align"></div></td><td><div class="td-align"><a data-id="${item.id}" class="more-btn"><img src="static/assets/more-icon.svg" class="rotate-90"></a></div></td></tr>`
19
+
20
+ if (isTrash) {
21
+ html += `<div data-path="${item.path}" id="more-option-${item.id}" data-name="${item.name}" class="more-options"><input class="more-options-focus" readonly="readonly" style="height:0;width:0;border:none;position:absolute"><div id="restore-${item.id}" data-path="${item.path}"><img src="static/assets/load-icon.svg"> Restore</div><hr><div id="delete-${item.id}" data-path="${item.path}"><img src="static/assets/trash-icon.svg"> Delete</div></div>`
22
+ }
23
+ else {
24
+ html += `<div data-path="${item.path}" id="more-option-${item.id}" data-name="${item.name}" class="more-options"><input class="more-options-focus" readonly="readonly" style="height:0;width:0;border:none;position:absolute"><div id="rename-${item.id}"><img src="static/assets/pencil-icon.svg"> Rename</div><hr><div id="trash-${item.id}"><img src="static/assets/trash-icon.svg"> Trash</div><hr><div id="folder-share-${item.id}"><img src="static/assets/share-icon.svg"> Share</div></div>`
25
+ }
26
+ }
27
+ }
28
+
29
+ for (const [key, item] of files) {
30
+ if (item.type === 'file') {
31
+ const size = convertBytes(item.size)
32
+ html += `<tr data-path="${item.path}" data-id="${item.id}" data-name="${item.name}" class="body-tr file-tr"><td><div class="td-align"><img src="static/assets/file-icon.svg">${item.name}</div></td><td><div class="td-align">${size}</div></td><td><div class="td-align"><a data-id="${item.id}" class="more-btn"><img src="static/assets/more-icon.svg" class="rotate-90"></a></div></td></tr>`
33
+
34
+ if (isTrash) {
35
+ html += `<div data-path="${item.path}" id="more-option-${item.id}" data-name="${item.name}" class="more-options"><input class="more-options-focus" readonly="readonly" style="height:0;width:0;border:none;position:absolute"><div id="restore-${item.id}" data-path="${item.path}"><img src="static/assets/load-icon.svg"> Restore</div><hr><div id="delete-${item.id}" data-path="${item.path}"><img src="static/assets/trash-icon.svg"> Delete</div></div>`
36
+ }
37
+ else {
38
+ html += `<div data-path="${item.path}" id="more-option-${item.id}" data-name="${item.name}" class="more-options"><input class="more-options-focus" readonly="readonly" style="height:0;width:0;border:none;position:absolute"><div id="rename-${item.id}"><img src="static/assets/pencil-icon.svg"> Rename</div><hr><div id="trash-${item.id}"><img src="static/assets/trash-icon.svg"> Trash</div><hr><div id="share-${item.id}"><img src="static/assets/share-icon.svg"> Share</div></div>`
39
+ }
40
+ }
41
+ }
42
+ document.getElementById('directory-data').innerHTML = html
43
+
44
+ if (!isTrash) {
45
+ document.querySelectorAll('.folder-tr').forEach(div => {
46
+ div.ondblclick = openFolder;
47
+ });
48
+ document.querySelectorAll('.file-tr').forEach(div => {
49
+ div.ondblclick = openFile;
50
+ });
51
+ }
52
+
53
+ document.querySelectorAll('.more-btn').forEach(div => {
54
+ div.addEventListener('click', function (event) {
55
+ event.preventDefault();
56
+ openMoreButton(div)
57
+ });
58
+ });
59
+ }
60
+
61
+ document.getElementById('search-form').addEventListener('submit', async (event) => {
62
+ event.preventDefault();
63
+ const query = document.getElementById('file-search').value;
64
+ console.log(query)
65
+ if (query === '') {
66
+ alert('Search field is empty');
67
+ return;
68
+ }
69
+ const path = '/?path=/search_' + encodeURI(query);
70
+ console.log(path)
71
+ window.location = path;
72
+ });
73
+
74
+ // Loading Main Page
75
+
76
+ document.addEventListener('DOMContentLoaded', function () {
77
+ const inputs = ['new-folder-name', 'rename-name', 'file-search']
78
+ for (let i = 0; i < inputs.length; i++) {
79
+ document.getElementById(inputs[i]).addEventListener('input', validateInput);
80
+ }
81
+
82
+ if (getCurrentPath().includes('/share_')) {
83
+ getCurrentDirectory()
84
+ } else {
85
+ if (getPassword() === null) {
86
+ document.getElementById('bg-blur').style.zIndex = '2';
87
+ document.getElementById('bg-blur').style.opacity = '0.1';
88
+
89
+ document.getElementById('get-password').style.zIndex = '3';
90
+ document.getElementById('get-password').style.opacity = '1';
91
+ } else {
92
+ getCurrentDirectory()
93
+ }
94
+ }
95
+ });
website/static/js/sidebar.js ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Handling New Button On Sidebar Click
2
+ const isTrash = getCurrentPath().startsWith('/trash')
3
+ const isSearch = getCurrentPath().startsWith('/search')
4
+ const isShare = getCurrentPath().startsWith('/share')
5
+
6
+ if (!isTrash && !isSearch) {
7
+ document.getElementById('new-button').addEventListener('click', () => {
8
+ document.getElementById('new-upload').style.zIndex = '1'
9
+ document.getElementById('new-upload').style.opacity = '1'
10
+ document.getElementById('new-upload').style.top = '80px'
11
+ document.getElementById('new-upload-focus').focus()
12
+ });
13
+ }
14
+ else {
15
+ document.getElementById('new-button').style.display = 'none'
16
+ }
17
+
18
+ if (isShare) {
19
+ document.getElementById('new-button').style.display = 'none'
20
+ const sections = document.querySelector('.sidebar-menu').getElementsByTagName('a')
21
+ sections[1].remove()
22
+ }
23
+
24
+ // New File Upload Start
25
+
26
+ function closeNewUploadFocus() {
27
+ setTimeout(() => {
28
+ document.getElementById('new-upload').style.opacity = '0'
29
+ document.getElementById('new-upload').style.top = '40px'
30
+ setTimeout(() => {
31
+ document.getElementById('new-upload').style.zIndex = '-1'
32
+ }, 300)
33
+ }, 200)
34
+ }
35
+ document.getElementById('new-upload-focus').addEventListener('blur', closeNewUploadFocus);
36
+ document.getElementById('new-upload-focus').addEventListener('focusout', closeNewUploadFocus);
37
+
38
+ document.getElementById('file-upload-btn').addEventListener('click', () => {
39
+ document.getElementById('fileInput').click()
40
+ });
41
+
42
+ // New File Upload End
43
+
44
+ // New Folder Start
45
+
46
+ document.getElementById('new-folder-btn').addEventListener('click', () => {
47
+ document.getElementById('new-folder-name').value = '';
48
+ document.getElementById('bg-blur').style.zIndex = '2';
49
+ document.getElementById('bg-blur').style.opacity = '0.1';
50
+
51
+ document.getElementById('create-new-folder').style.zIndex = '3';
52
+ document.getElementById('create-new-folder').style.opacity = '1';
53
+ setTimeout(() => {
54
+ document.getElementById('new-folder-name').focus();
55
+ }, 300)
56
+ })
57
+
58
+ document.getElementById('new-folder-cancel').addEventListener('click', () => {
59
+ document.getElementById('new-folder-name').value = '';
60
+ document.getElementById('bg-blur').style.opacity = '0';
61
+ setTimeout(() => {
62
+ document.getElementById('bg-blur').style.zIndex = '-1';
63
+ }, 300)
64
+ document.getElementById('create-new-folder').style.opacity = '0';
65
+ setTimeout(() => {
66
+ document.getElementById('create-new-folder').style.zIndex = '-1';
67
+ }, 300)
68
+ });
69
+
70
+ document.getElementById('new-folder-create').addEventListener('click', createNewFolder);
71
+
72
+ // New Folder End
73
+
74
+ // New Url Upload Start
75
+
76
+ document.getElementById('url-upload-btn').addEventListener('click', () => {
77
+ document.getElementById('remote-url').value = '';
78
+ document.getElementById('bg-blur').style.zIndex = '2';
79
+ document.getElementById('bg-blur').style.opacity = '0.1';
80
+
81
+ document.getElementById('new-url-upload').style.zIndex = '3';
82
+ document.getElementById('new-url-upload').style.opacity = '1';
83
+ setTimeout(() => {
84
+ document.getElementById('remote-url').focus();
85
+ }, 300)
86
+ })
87
+
88
+ document.getElementById('remote-cancel').addEventListener('click', () => {
89
+ document.getElementById('remote-url').value = '';
90
+ document.getElementById('bg-blur').style.opacity = '0';
91
+ setTimeout(() => {
92
+ document.getElementById('bg-blur').style.zIndex = '-1';
93
+ }, 300)
94
+ document.getElementById('new-url-upload').style.opacity = '0';
95
+ setTimeout(() => {
96
+ document.getElementById('new-url-upload').style.zIndex = '-1';
97
+ }, 300)
98
+ });
99
+
100
+ document.getElementById('remote-start').addEventListener('click', Start_URL_Upload);
101
+
102
+ // New Url Upload End