shdir 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of shdir might be problematic. Click here for more details.
- shdir-0.1.0/PKG-INFO +11 -0
- shdir-0.1.0/pyproject.toml +16 -0
- shdir-0.1.0/src/shdir/.gitignore +220 -0
- shdir-0.1.0/src/shdir/__init__.py +0 -0
- shdir-0.1.0/src/shdir/app.py +280 -0
- shdir-0.1.0/src/shdir/cli.py +75 -0
- shdir-0.1.0/src/shdir/files.py +0 -0
- shdir-0.1.0/src/shdir/icons/arrow_up.svg +2 -0
- shdir-0.1.0/src/shdir/icons/file.svg +1 -0
- shdir-0.1.0/src/shdir/icons/folder.svg +1 -0
- shdir-0.1.0/src/shdir/icons/python.svg +1 -0
- shdir-0.1.0/src/shdir/icons/search.svg +1 -0
- shdir-0.1.0/src/shdir/icons/zip.svg +1 -0
- shdir-0.1.0/src/shdir/static/login-style.css +210 -0
- shdir-0.1.0/src/shdir/static/login_view.js +11 -0
- shdir-0.1.0/src/shdir/static/script.js +15 -0
- shdir-0.1.0/src/shdir/static/styles.css +425 -0
- shdir-0.1.0/src/shdir/templates/index.html +98 -0
- shdir-0.1.0/src/shdir/templates/login-page.html +70 -0
- shdir-0.1.0/src/shdir/utils/colors.py +24 -0
- shdir-0.1.0/src/shdir/utils/logs.py +24 -0
- shdir-0.1.0/src/shdir/utils/net.py +15 -0
- shdir-0.1.0/src/shdir/utils/security.py +71 -0
shdir-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shdir
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Share your files in local network with secure connection
|
|
5
|
+
License: GNU GPL v3
|
|
6
|
+
Author: Rogov Anton
|
|
7
|
+
Author-email: akldrix@gmail.com
|
|
8
|
+
Requires-Python: >=3.15
|
|
9
|
+
Classifier: License :: Other/Proprietary License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "shdir"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Share your files in local network with secure connection"
|
|
5
|
+
authors = [{ name = "Rogov Anton", email = "akldrix@gmail.com" }]
|
|
6
|
+
license = "GNU GPL v3"
|
|
7
|
+
requires-python = ">=3.15"
|
|
8
|
+
dependencies = []
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
[build-system]
|
|
12
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
13
|
+
build-backend = "poetry.core.masonry.api"
|
|
14
|
+
|
|
15
|
+
[tool.poetry.scripts]
|
|
16
|
+
APPLICATION-NAME = 'shdir.app:main'
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
*.lcov
|
|
51
|
+
.hypothesis/
|
|
52
|
+
.pytest_cache/
|
|
53
|
+
cover/
|
|
54
|
+
|
|
55
|
+
# Translations
|
|
56
|
+
*.mo
|
|
57
|
+
*.pot
|
|
58
|
+
|
|
59
|
+
# Django stuff:
|
|
60
|
+
*.log
|
|
61
|
+
local_settings.py
|
|
62
|
+
db.sqlite3
|
|
63
|
+
db.sqlite3-journal
|
|
64
|
+
|
|
65
|
+
# Flask stuff:
|
|
66
|
+
instance/
|
|
67
|
+
.webassets-cache
|
|
68
|
+
|
|
69
|
+
# Scrapy stuff:
|
|
70
|
+
.scrapy
|
|
71
|
+
|
|
72
|
+
# Sphinx documentation
|
|
73
|
+
docs/_build/
|
|
74
|
+
|
|
75
|
+
# PyBuilder
|
|
76
|
+
.pybuilder/
|
|
77
|
+
target/
|
|
78
|
+
|
|
79
|
+
# Jupyter Notebook
|
|
80
|
+
.ipynb_checkpoints
|
|
81
|
+
|
|
82
|
+
# IPython
|
|
83
|
+
profile_default/
|
|
84
|
+
ipython_config.py
|
|
85
|
+
|
|
86
|
+
# pyenv
|
|
87
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
88
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
89
|
+
# .python-version
|
|
90
|
+
|
|
91
|
+
# pipenv
|
|
92
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
93
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
94
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
95
|
+
# install all needed dependencies.
|
|
96
|
+
# Pipfile.lock
|
|
97
|
+
|
|
98
|
+
# uv
|
|
99
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
100
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
101
|
+
# commonly ignored for libraries.
|
|
102
|
+
# uv.lock
|
|
103
|
+
|
|
104
|
+
# poetry
|
|
105
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
106
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
107
|
+
# commonly ignored for libraries.
|
|
108
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
109
|
+
# poetry.lock
|
|
110
|
+
# poetry.toml
|
|
111
|
+
|
|
112
|
+
# pdm
|
|
113
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
114
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
115
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
116
|
+
# pdm.lock
|
|
117
|
+
# pdm.toml
|
|
118
|
+
.pdm-python
|
|
119
|
+
.pdm-build/
|
|
120
|
+
|
|
121
|
+
# pixi
|
|
122
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
123
|
+
# pixi.lock
|
|
124
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
125
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
126
|
+
.pixi/*
|
|
127
|
+
!.pixi/config.toml
|
|
128
|
+
|
|
129
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
130
|
+
__pypackages__/
|
|
131
|
+
|
|
132
|
+
# Celery stuff
|
|
133
|
+
celerybeat-schedule*
|
|
134
|
+
celerybeat.pid
|
|
135
|
+
|
|
136
|
+
# Redis
|
|
137
|
+
*.rdb
|
|
138
|
+
*.aof
|
|
139
|
+
*.pid
|
|
140
|
+
|
|
141
|
+
# RabbitMQ
|
|
142
|
+
mnesia/
|
|
143
|
+
rabbitmq/
|
|
144
|
+
rabbitmq-data/
|
|
145
|
+
|
|
146
|
+
# ActiveMQ
|
|
147
|
+
activemq-data/
|
|
148
|
+
|
|
149
|
+
# SageMath parsed files
|
|
150
|
+
*.sage.py
|
|
151
|
+
|
|
152
|
+
# Environments
|
|
153
|
+
.env
|
|
154
|
+
.envrc
|
|
155
|
+
.venv
|
|
156
|
+
env/
|
|
157
|
+
venv/
|
|
158
|
+
ENV/
|
|
159
|
+
env.bak/
|
|
160
|
+
venv.bak/
|
|
161
|
+
|
|
162
|
+
# Spyder project settings
|
|
163
|
+
.spyderproject
|
|
164
|
+
.spyproject
|
|
165
|
+
|
|
166
|
+
# Rope project settings
|
|
167
|
+
.ropeproject
|
|
168
|
+
|
|
169
|
+
# mkdocs/Zensical documentation
|
|
170
|
+
/site
|
|
171
|
+
|
|
172
|
+
# mypy
|
|
173
|
+
.mypy_cache/
|
|
174
|
+
.dmypy.json
|
|
175
|
+
dmypy.json
|
|
176
|
+
|
|
177
|
+
# Pyre type checker
|
|
178
|
+
.pyre/
|
|
179
|
+
|
|
180
|
+
# pytype static type analyzer
|
|
181
|
+
.pytype/
|
|
182
|
+
|
|
183
|
+
# Cython debug symbols
|
|
184
|
+
cython_debug/
|
|
185
|
+
|
|
186
|
+
# PyCharm
|
|
187
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
188
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
189
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
190
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
191
|
+
# .idea/
|
|
192
|
+
|
|
193
|
+
# Abstra
|
|
194
|
+
# Abstra is an AI-powered process automation framework.
|
|
195
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
196
|
+
# Learn more at https://abstra.io/docs
|
|
197
|
+
.abstra/
|
|
198
|
+
|
|
199
|
+
# Visual Studio Code
|
|
200
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore that
|
|
201
|
+
# can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
202
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer, you
|
|
203
|
+
# could uncomment the following to ignore the entire vscode folder
|
|
204
|
+
# .vscode/
|
|
205
|
+
# Temporary file for partial code execution
|
|
206
|
+
tempCodeRunnerFile.py
|
|
207
|
+
|
|
208
|
+
# Ruff stuff:
|
|
209
|
+
.ruff_cache/
|
|
210
|
+
|
|
211
|
+
# PyPI configuration file
|
|
212
|
+
.pypirc
|
|
213
|
+
|
|
214
|
+
# Marimo
|
|
215
|
+
marimo/_static/
|
|
216
|
+
marimo/_lsp/
|
|
217
|
+
__marimo__/
|
|
218
|
+
|
|
219
|
+
# Streamlit
|
|
220
|
+
.streamlit/secrets.toml
|
|
File without changes
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import os
|
|
3
|
+
import secrets
|
|
4
|
+
import shutil
|
|
5
|
+
import urllib
|
|
6
|
+
import zipfile
|
|
7
|
+
from contextlib import asynccontextmanager
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import uvicorn
|
|
11
|
+
from fastapi import (
|
|
12
|
+
FastAPI,
|
|
13
|
+
Form,
|
|
14
|
+
HTTPException,
|
|
15
|
+
Request,
|
|
16
|
+
UploadFile,
|
|
17
|
+
)
|
|
18
|
+
from fastapi.responses import (
|
|
19
|
+
FileResponse,
|
|
20
|
+
HTMLResponse,
|
|
21
|
+
RedirectResponse,
|
|
22
|
+
StreamingResponse,
|
|
23
|
+
)
|
|
24
|
+
from fastapi.staticfiles import StaticFiles
|
|
25
|
+
from fastapi.templating import Jinja2Templates
|
|
26
|
+
|
|
27
|
+
from cli import args, make_qr
|
|
28
|
+
from utils.colors import BLUE, CYAN, RESET
|
|
29
|
+
from utils.logs import log_config, logger
|
|
30
|
+
from utils.security import temp_tls_cert
|
|
31
|
+
|
|
32
|
+
COOKIE_NAME = "shdir_session"
|
|
33
|
+
IS_AUTH_ENABLED = args.auth
|
|
34
|
+
IS_HTTPS_ENABLED = args.https
|
|
35
|
+
IS_UPLOAD_DISABLED = args.no_upload
|
|
36
|
+
if args.dir:
|
|
37
|
+
dir_path = Path(args.dir)
|
|
38
|
+
if dir_path.is_dir():
|
|
39
|
+
BASE_DIR = dir_path.resolve()
|
|
40
|
+
else:
|
|
41
|
+
raise NotADirectoryError(
|
|
42
|
+
f"This path is not a dir or does not exist: {dir_path}"
|
|
43
|
+
)
|
|
44
|
+
else:
|
|
45
|
+
BASE_DIR = Path.cwd().resolve()
|
|
46
|
+
|
|
47
|
+
logger.info(f"\n{CYAN} Sharing Directory: {BASE_DIR}{RESET}")
|
|
48
|
+
|
|
49
|
+
TEMPLATE_DIR = Path(__file__).resolve().parent / "templates"
|
|
50
|
+
STATIC_DIR = Path(__file__).resolve().parent / "static"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if args.code:
|
|
54
|
+
IS_AUTH_ENABLED = True
|
|
55
|
+
SERVER_CODE = args.code
|
|
56
|
+
logger.info(f"\n{BLUE} ACCESS CODE: {SERVER_CODE}{RESET}")
|
|
57
|
+
elif IS_AUTH_ENABLED:
|
|
58
|
+
SERVER_CODE = "".join(secrets.choice("0123456789") for _ in range(6))
|
|
59
|
+
logger.info(f"\n{BLUE} ACCESS CODE: {SERVER_CODE}{RESET}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def check_authenticated(request: Request):
|
|
63
|
+
if IS_AUTH_ENABLED:
|
|
64
|
+
session_token = request.cookies.get(COOKIE_NAME)
|
|
65
|
+
|
|
66
|
+
if not session_token or not secrets.compare_digest(session_token, SERVER_CODE):
|
|
67
|
+
raise HTTPException(status_code=401, detail="Not authenticated")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@asynccontextmanager
|
|
71
|
+
async def lifespan(app: FastAPI):
|
|
72
|
+
make_qr()
|
|
73
|
+
logger.info(f"\n{BLUE} Server is running. Press Ctrl + C to stop...{RESET}\n")
|
|
74
|
+
yield
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
app = FastAPI(lifespan=lifespan)
|
|
78
|
+
|
|
79
|
+
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
|
80
|
+
|
|
81
|
+
templates = Jinja2Templates(directory=TEMPLATE_DIR)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@app.get("/login", response_class=HTMLResponse)
|
|
85
|
+
async def login_page(request: Request):
|
|
86
|
+
if IS_AUTH_ENABLED and request.cookies.get(COOKIE_NAME) == SERVER_CODE:
|
|
87
|
+
return RedirectResponse(url="/", status_code=303)
|
|
88
|
+
|
|
89
|
+
return templates.TemplateResponse(request=request, name="login-page.html")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@app.post("/login")
|
|
93
|
+
async def login_process(request: Request, code: str = Form(...)):
|
|
94
|
+
|
|
95
|
+
if secrets.compare_digest(code, SERVER_CODE):
|
|
96
|
+
response = RedirectResponse("/", status_code=303)
|
|
97
|
+
response.set_cookie(
|
|
98
|
+
key=COOKIE_NAME,
|
|
99
|
+
value=SERVER_CODE,
|
|
100
|
+
httponly=True,
|
|
101
|
+
samesite="lax",
|
|
102
|
+
)
|
|
103
|
+
return response
|
|
104
|
+
return templates.TemplateResponse(
|
|
105
|
+
request=request,
|
|
106
|
+
name="login-page.html",
|
|
107
|
+
context={"error": True},
|
|
108
|
+
status_code=401,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@app.get("/", response_class=HTMLResponse)
|
|
113
|
+
@app.get("/{subpath:path}", response_class=HTMLResponse)
|
|
114
|
+
async def get_files_page(request: Request, subpath: str = ""):
|
|
115
|
+
try:
|
|
116
|
+
check_authenticated(request)
|
|
117
|
+
except HTTPException as e:
|
|
118
|
+
if e.status_code == 401:
|
|
119
|
+
return RedirectResponse(url="/login", status_code=303)
|
|
120
|
+
raise e
|
|
121
|
+
|
|
122
|
+
zip_target = request.query_params.get("zip")
|
|
123
|
+
if zip_target:
|
|
124
|
+
target_path = (BASE_DIR / zip_target).resolve()
|
|
125
|
+
|
|
126
|
+
if (
|
|
127
|
+
not target_path.relative_to(BASE_DIR)
|
|
128
|
+
or not target_path.exists()
|
|
129
|
+
or not target_path.is_dir()
|
|
130
|
+
):
|
|
131
|
+
raise HTTPException(
|
|
132
|
+
status_code=403, detail="Access denied or invalid directory"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
zip_buffer = io.BytesIO()
|
|
136
|
+
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
137
|
+
for f_path in target_path.rglob("*"):
|
|
138
|
+
archive_name = f_path.relative_to(target_path)
|
|
139
|
+
if f_path.is_file():
|
|
140
|
+
zf.write(f_path, archive_name)
|
|
141
|
+
|
|
142
|
+
zip_buffer.seek(0)
|
|
143
|
+
archive_filename = f"{target_path.name or 'root'}.zip"
|
|
144
|
+
|
|
145
|
+
encoded_filename = urllib.parse.quote(archive_filename)
|
|
146
|
+
|
|
147
|
+
return StreamingResponse(
|
|
148
|
+
zip_buffer,
|
|
149
|
+
media_type="application/x-zip-compressed",
|
|
150
|
+
headers={
|
|
151
|
+
"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}"
|
|
152
|
+
},
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
target_path = (BASE_DIR / subpath).resolve()
|
|
156
|
+
|
|
157
|
+
if not target_path.is_relative_to(BASE_DIR):
|
|
158
|
+
raise HTTPException(status_code=403, detail="Access denied")
|
|
159
|
+
if not target_path.exists():
|
|
160
|
+
raise HTTPException(status_code=404, detail="Not found")
|
|
161
|
+
if target_path.is_file():
|
|
162
|
+
return FileResponse(
|
|
163
|
+
path=target_path,
|
|
164
|
+
filename=target_path.name,
|
|
165
|
+
media_type="application/octet-stream",
|
|
166
|
+
)
|
|
167
|
+
if target_path != BASE_DIR:
|
|
168
|
+
parent_rel = target_path.parent.relative_to(BASE_DIR)
|
|
169
|
+
parent_path = (
|
|
170
|
+
str(parent_rel).replace("\\", "/") if str(parent_rel) != "." else ""
|
|
171
|
+
)
|
|
172
|
+
else:
|
|
173
|
+
parent_path = ""
|
|
174
|
+
|
|
175
|
+
dirs = []
|
|
176
|
+
files = []
|
|
177
|
+
|
|
178
|
+
for path in target_path.iterdir():
|
|
179
|
+
rel_path = path.relative_to(BASE_DIR)
|
|
180
|
+
web_rel_path = str(rel_path).replace("\\", "/")
|
|
181
|
+
|
|
182
|
+
if path.is_dir():
|
|
183
|
+
dirs.append(
|
|
184
|
+
{
|
|
185
|
+
"name": path.name,
|
|
186
|
+
"path": web_rel_path,
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
elif path.is_file():
|
|
190
|
+
_, ext = os.path.splitext(path)
|
|
191
|
+
files.append(
|
|
192
|
+
{
|
|
193
|
+
"name": path.name,
|
|
194
|
+
"path": web_rel_path,
|
|
195
|
+
"ext": ext,
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
path_len = [p for p in subpath.split("/") if p]
|
|
199
|
+
depth = len(path_len)
|
|
200
|
+
context = {
|
|
201
|
+
"files": files,
|
|
202
|
+
"dirs": dirs,
|
|
203
|
+
"subpath": subpath,
|
|
204
|
+
"parent_path": parent_path,
|
|
205
|
+
"is_root": target_path == BASE_DIR,
|
|
206
|
+
"root": "",
|
|
207
|
+
"show_home": depth >= 2,
|
|
208
|
+
"upload": not IS_UPLOAD_DISABLED,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return templates.TemplateResponse(
|
|
212
|
+
request=request,
|
|
213
|
+
name="index.html",
|
|
214
|
+
context=context,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@app.post("/")
|
|
219
|
+
@app.post("/{subpath:path}")
|
|
220
|
+
async def upload_file(
|
|
221
|
+
request: Request,
|
|
222
|
+
files: list[UploadFile],
|
|
223
|
+
subpath: str = "",
|
|
224
|
+
):
|
|
225
|
+
if not IS_UPLOAD_DISABLED:
|
|
226
|
+
try:
|
|
227
|
+
check_authenticated(request)
|
|
228
|
+
except HTTPException as e:
|
|
229
|
+
if e.status_code == 401:
|
|
230
|
+
return RedirectResponse(url="/login", status_code=303)
|
|
231
|
+
raise e
|
|
232
|
+
|
|
233
|
+
target_path = (BASE_DIR / subpath).resolve()
|
|
234
|
+
|
|
235
|
+
if not target_path.is_relative_to(BASE_DIR):
|
|
236
|
+
raise HTTPException(status_code=403, detail="Access denied")
|
|
237
|
+
if not target_path.exists():
|
|
238
|
+
raise HTTPException(status_code=404, detail="Not found")
|
|
239
|
+
for file in files:
|
|
240
|
+
if not file.filename:
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
dest_path = target_path / file.filename
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
with dest_path.open("wb") as buf:
|
|
247
|
+
shutil.copyfileobj(file.file, buf)
|
|
248
|
+
except Exception as e:
|
|
249
|
+
logger.error(f"Error occured while loading file: {e}")
|
|
250
|
+
raise HTTPException(status_code=500, detail="Could not save file")
|
|
251
|
+
finally:
|
|
252
|
+
await file.close()
|
|
253
|
+
|
|
254
|
+
return RedirectResponse(url=request.url.path, status_code=303)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def run_app():
|
|
258
|
+
if IS_HTTPS_ENABLED:
|
|
259
|
+
with temp_tls_cert() as (cert_path, key_path):
|
|
260
|
+
uvicorn.run(
|
|
261
|
+
"app:app",
|
|
262
|
+
host="0.0.0.0",
|
|
263
|
+
port=8000,
|
|
264
|
+
reload=False,
|
|
265
|
+
log_config=log_config,
|
|
266
|
+
ssl_certfile=cert_path,
|
|
267
|
+
ssl_keyfile=key_path,
|
|
268
|
+
)
|
|
269
|
+
else:
|
|
270
|
+
uvicorn.run(
|
|
271
|
+
"app:app",
|
|
272
|
+
host="0.0.0.0",
|
|
273
|
+
port=8000,
|
|
274
|
+
reload=False,
|
|
275
|
+
log_config=log_config,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
if __name__ == "__main__":
|
|
280
|
+
run_app()
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
import qrcode
|
|
4
|
+
|
|
5
|
+
from utils.colors import GREEN, RESET, UNDERLINE
|
|
6
|
+
from utils.logs import logger
|
|
7
|
+
from utils.net import get_local_ip
|
|
8
|
+
|
|
9
|
+
parser = argparse.ArgumentParser(description="Shdir file manager")
|
|
10
|
+
parser.add_argument(
|
|
11
|
+
"--code",
|
|
12
|
+
"-c",
|
|
13
|
+
type=str,
|
|
14
|
+
default=None,
|
|
15
|
+
help="Access code for current session",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"--auth",
|
|
20
|
+
"-a",
|
|
21
|
+
action="store_true",
|
|
22
|
+
help="Run server with authentication",
|
|
23
|
+
)
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"--https",
|
|
26
|
+
action="store_true",
|
|
27
|
+
help="Run server with temporary tsl certificate",
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--dir",
|
|
31
|
+
"-d",
|
|
32
|
+
type=str,
|
|
33
|
+
default=None,
|
|
34
|
+
help="Set up a directory to share",
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"--no-upload",
|
|
38
|
+
"-u",
|
|
39
|
+
action="store_true",
|
|
40
|
+
help="Run without file upload",
|
|
41
|
+
)
|
|
42
|
+
args, _ = parser.parse_known_args()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def make_qr():
|
|
46
|
+
is_https = args.https
|
|
47
|
+
qr = qrcode.QRCode(version=1, box_size=1, border=1)
|
|
48
|
+
|
|
49
|
+
if is_https:
|
|
50
|
+
qr.add_data(f"https://{get_local_ip()}:8000/")
|
|
51
|
+
logger.info(
|
|
52
|
+
f"\n{UNDERLINE}{GREEN} Copy Link: https://{get_local_ip()}:8000/{RESET}\n\n"
|
|
53
|
+
)
|
|
54
|
+
else:
|
|
55
|
+
qr.add_data(f"http://{get_local_ip()}:8000/")
|
|
56
|
+
logger.info(
|
|
57
|
+
f"\n{UNDERLINE}{GREEN} Copy Link: http://{get_local_ip()}:8000/{RESET}\n\n"
|
|
58
|
+
)
|
|
59
|
+
qr.make(fit=True)
|
|
60
|
+
|
|
61
|
+
matrix = qr.get_matrix()
|
|
62
|
+
|
|
63
|
+
WHITE_BLOCK = "\033[7m \033[m"
|
|
64
|
+
BLACK_BLOCK = " "
|
|
65
|
+
|
|
66
|
+
indent = " "
|
|
67
|
+
|
|
68
|
+
qr_lines = []
|
|
69
|
+
for row in matrix:
|
|
70
|
+
line = "".join(BLACK_BLOCK if cell else WHITE_BLOCK for cell in row)
|
|
71
|
+
qr_lines.append(f"{indent}{line}")
|
|
72
|
+
|
|
73
|
+
tabulated_qr = "\n".join(qr_lines)
|
|
74
|
+
|
|
75
|
+
logger.info(f"{tabulated_qr}")
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file preview-icon"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-folder preview-icon"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Python</title><path d="M14.25.18l.9.2.73.26.59.3.45.32.34.34.25.34.16.33.1.3.04.26.02.2-.01.13V8.5l-.05.63-.13.55-.21.46-.26.38-.3.31-.33.25-.35.19-.35.14-.33.1-.3.07-.26.04-.21.02H8.77l-.69.05-.59.14-.5.22-.41.27-.33.32-.27.35-.2.36-.15.37-.1.35-.07.32-.04.27-.02.21v3.06H3.17l-.21-.03-.28-.07-.32-.12-.35-.18-.36-.26-.36-.36-.35-.46-.32-.59-.28-.73-.21-.88-.14-1.05-.05-1.23.06-1.22.16-1.04.24-.87.32-.71.36-.57.4-.44.42-.33.42-.24.4-.16.36-.1.32-.05.24-.01h.16l.06.01h8.16v-.83H6.18l-.01-2.75-.02-.37.05-.34.11-.31.17-.28.25-.26.31-.23.38-.2.44-.18.51-.15.58-.12.64-.1.71-.06.77-.04.84-.02 1.27.05zm-6.3 1.98l-.23.33-.08.41.08.41.23.34.33.22.41.09.41-.09.33-.22.23-.34.08-.41-.08-.41-.23-.33-.33-.22-.41-.09-.41.09zm13.09 3.95l.28.06.32.12.35.18.36.27.36.35.35.47.32.59.28.73.21.88.14 1.04.05 1.23-.06 1.23-.16 1.04-.24.86-.32.71-.36.57-.4.45-.42.33-.42.24-.4.16-.36.09-.32.05-.24.02-.16-.01h-8.22v.82h5.84l.01 2.76.02.36-.05.34-.11.31-.17.29-.25.25-.31.24-.38.2-.44.17-.51.15-.58.13-.64.09-.71.07-.77.04-.84.01-1.27-.04-1.07-.14-.9-.2-.73-.25-.59-.3-.45-.33-.34-.34-.25-.34-.16-.33-.1-.3-.04-.25-.02-.2.01-.13v-5.34l.05-.64.13-.54.21-.46.26-.38.3-.32.33-.24.35-.2.35-.14.33-.1.3-.06.26-.04.21-.02.13-.01h5.84l.69-.05.59-.14.5-.21.41-.28.33-.32.27-.35.2-.36.15-.36.1-.35.07-.32.04-.28.02-.21V6.07h2.09l.14.01zm-6.47 14.25l-.23.33-.08.41.08.41.23.33.33.23.41.08.41-.08.33-.23.23-.33.08-.41-.08-.41-.23-.33-.33-.23-.41-.08-.41.08z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search preview-icon"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-archive preview-icon"><path d="M13.659 22H18a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v11.5"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="M8 12v-1"/><path d="M8 18v-2"/><path d="M8 7V6"/><circle cx="8" cy="20" r="2"/></svg>
|