simple-image-hosting 0.1.1__py3-none-any.whl
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.
- simple_image/__init__.py +3 -0
- simple_image/cli.py +171 -0
- simple_image/main.py +851 -0
- simple_image/models.py +125 -0
- simple_image/utils.py +146 -0
- simple_image/web/index.html +415 -0
- simple_image/web/static/app.js +1590 -0
- simple_image/web/static/styles.css +766 -0
- simple_image/web/static/vendor/axios.min.js +5 -0
- simple_image/web/static/vendor/element-plus.css +1 -0
- simple_image/web/static/vendor/element-plus.full.min.js +43 -0
- simple_image/web/static/vendor/heic2any.min.js +1 -0
- simple_image/web/static/vendor/load-image.all.min.js +2 -0
- simple_image/web/static/vendor/vue.global.prod.js +13 -0
- simple_image_hosting-0.1.1.dist-info/METADATA +207 -0
- simple_image_hosting-0.1.1.dist-info/RECORD +20 -0
- simple_image_hosting-0.1.1.dist-info/WHEEL +5 -0
- simple_image_hosting-0.1.1.dist-info/entry_points.txt +2 -0
- simple_image_hosting-0.1.1.dist-info/licenses/LICENSE +201 -0
- simple_image_hosting-0.1.1.dist-info/top_level.txt +1 -0
simple_image/__init__.py
ADDED
simple_image/cli.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import getpass
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import uvicorn
|
|
8
|
+
|
|
9
|
+
from .main import create_app, hash_password
|
|
10
|
+
from .models import DEFAULT_COMPRESS_QUALITY, User, create_session_factory
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
14
|
+
parser = argparse.ArgumentParser(prog="simple-image", description="Simple Image server CLI")
|
|
15
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
16
|
+
|
|
17
|
+
serve = subparsers.add_parser("serve", help="Run HTTP server")
|
|
18
|
+
serve.add_argument("data_dir", type=Path, help="Runtime data directory (DB + images)")
|
|
19
|
+
serve.add_argument("--host", default="0.0.0.0", help="Bind host")
|
|
20
|
+
serve.add_argument("--port", type=int, default=8000, help="Bind port")
|
|
21
|
+
serve.add_argument("--reload", action="store_true", help="Enable auto reload")
|
|
22
|
+
serve.add_argument("--api-url", default=None, help="Public API base URL")
|
|
23
|
+
serve.add_argument("--admin-username", default=None, help="Bootstrap admin username")
|
|
24
|
+
serve.add_argument("--admin-password", default=None, help="Bootstrap admin password")
|
|
25
|
+
serve.add_argument(
|
|
26
|
+
"--database-url",
|
|
27
|
+
default=None,
|
|
28
|
+
help="Database URL, e.g. mysql+pymysql://user:pass@host:3306/simple_image",
|
|
29
|
+
)
|
|
30
|
+
serve.add_argument(
|
|
31
|
+
"--compress-quality",
|
|
32
|
+
type=int,
|
|
33
|
+
default=None,
|
|
34
|
+
help="Default upload compress quality (1-95)",
|
|
35
|
+
)
|
|
36
|
+
serve.add_argument(
|
|
37
|
+
"--base-path",
|
|
38
|
+
default=None,
|
|
39
|
+
help="Deploy under sub path, e.g. /simple_image",
|
|
40
|
+
)
|
|
41
|
+
serve.add_argument(
|
|
42
|
+
"-d", "--daemon", action="store_true",
|
|
43
|
+
help="Run server as a background daemon",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
reset_pwd = subparsers.add_parser(
|
|
47
|
+
"reset-admin-password", help="Reset admin password"
|
|
48
|
+
)
|
|
49
|
+
reset_pwd.add_argument(
|
|
50
|
+
"data_dir", type=Path, help="Runtime data directory (same as used by serve)"
|
|
51
|
+
)
|
|
52
|
+
reset_pwd.add_argument(
|
|
53
|
+
"--username", default=None,
|
|
54
|
+
help="Admin username to reset (omit to auto-select the only admin)"
|
|
55
|
+
)
|
|
56
|
+
reset_pwd.add_argument(
|
|
57
|
+
"--database-url", default=None,
|
|
58
|
+
help="Database URL (omit to use data_dir/database.db)"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return parser
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _daemonize(data_dir: Path) -> None:
|
|
65
|
+
"""Double-fork to detach from the controlling terminal."""
|
|
66
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
pid_file = data_dir / "simple-image.pid"
|
|
68
|
+
log_file = data_dir / "simple-image.log"
|
|
69
|
+
|
|
70
|
+
# First fork – exit parent
|
|
71
|
+
if os.fork() > 0:
|
|
72
|
+
raise SystemExit(0)
|
|
73
|
+
|
|
74
|
+
os.setsid()
|
|
75
|
+
|
|
76
|
+
# Second fork – prevent re-acquiring a terminal
|
|
77
|
+
if os.fork() > 0:
|
|
78
|
+
raise SystemExit(0)
|
|
79
|
+
|
|
80
|
+
# Redirect stdio to log file
|
|
81
|
+
log_fd = os.open(str(log_file), os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o644)
|
|
82
|
+
os.dup2(log_fd, sys.stdout.fileno())
|
|
83
|
+
os.dup2(log_fd, sys.stderr.fileno())
|
|
84
|
+
os.close(log_fd)
|
|
85
|
+
|
|
86
|
+
# Write PID file
|
|
87
|
+
pid_file.write_text(str(os.getpid()))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def run_serve(args: argparse.Namespace) -> None:
|
|
91
|
+
if args.daemon:
|
|
92
|
+
_daemonize(args.data_dir)
|
|
93
|
+
|
|
94
|
+
app = create_app(
|
|
95
|
+
data_dir=args.data_dir,
|
|
96
|
+
api_url=args.api_url,
|
|
97
|
+
admin_username=args.admin_username,
|
|
98
|
+
admin_password=args.admin_password,
|
|
99
|
+
default_compress_quality=args.compress_quality,
|
|
100
|
+
database_url=args.database_url,
|
|
101
|
+
base_path=args.base_path,
|
|
102
|
+
)
|
|
103
|
+
uvicorn.run(app, host=args.host, port=args.port, reload=args.reload)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def run_reset_admin_password(args: argparse.Namespace) -> None:
|
|
107
|
+
db_path = args.data_dir / "database.db"
|
|
108
|
+
database_url = args.database_url or os.getenv("SIMPLE_IMAGE_DATABASE_URL") or os.getenv("DATABASE_URL")
|
|
109
|
+
if not database_url:
|
|
110
|
+
if not db_path.exists():
|
|
111
|
+
print(f"Error: database not found at {db_path}", file=sys.stderr)
|
|
112
|
+
raise SystemExit(1)
|
|
113
|
+
database_url = f"sqlite:///{db_path}"
|
|
114
|
+
|
|
115
|
+
use_db_path = None if args.database_url else db_path
|
|
116
|
+
session_factory = create_session_factory(
|
|
117
|
+
default_compress_quality=DEFAULT_COMPRESS_QUALITY,
|
|
118
|
+
database_url=database_url,
|
|
119
|
+
db_path=use_db_path,
|
|
120
|
+
)
|
|
121
|
+
db = session_factory()
|
|
122
|
+
try:
|
|
123
|
+
admins = db.query(User).filter(User.is_admin == True).all()
|
|
124
|
+
|
|
125
|
+
if not admins:
|
|
126
|
+
print("Error: no admin user found in the database.", file=sys.stderr)
|
|
127
|
+
raise SystemExit(1)
|
|
128
|
+
|
|
129
|
+
if args.username:
|
|
130
|
+
target = next((u for u in admins if u.username == args.username), None)
|
|
131
|
+
if not target:
|
|
132
|
+
available = ", ".join(u.username for u in admins)
|
|
133
|
+
print(f"Error: admin '{args.username}' not found. Existing admins: {available}", file=sys.stderr)
|
|
134
|
+
raise SystemExit(1)
|
|
135
|
+
elif len(admins) == 1:
|
|
136
|
+
target = admins[0]
|
|
137
|
+
print(f"Found admin user: {target.username}")
|
|
138
|
+
else:
|
|
139
|
+
print("Multiple admin users found, please specify --username:")
|
|
140
|
+
for u in admins:
|
|
141
|
+
print(f" - {u.username}")
|
|
142
|
+
raise SystemExit(1)
|
|
143
|
+
|
|
144
|
+
new_password = getpass.getpass(f"New password for '{target.username}': ")
|
|
145
|
+
if not new_password:
|
|
146
|
+
print("Error: password cannot be empty.", file=sys.stderr)
|
|
147
|
+
raise SystemExit(1)
|
|
148
|
+
confirm = getpass.getpass("Confirm new password: ")
|
|
149
|
+
if new_password != confirm:
|
|
150
|
+
print("Error: passwords do not match.", file=sys.stderr)
|
|
151
|
+
raise SystemExit(1)
|
|
152
|
+
|
|
153
|
+
target.password_hash = hash_password(new_password)
|
|
154
|
+
db.commit()
|
|
155
|
+
print(f"Password for admin '{target.username}' has been reset successfully.")
|
|
156
|
+
finally:
|
|
157
|
+
db.close()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def main() -> None:
|
|
161
|
+
parser = build_parser()
|
|
162
|
+
args = parser.parse_args()
|
|
163
|
+
|
|
164
|
+
if args.command == "serve":
|
|
165
|
+
run_serve(args)
|
|
166
|
+
elif args.command == "reset-admin-password":
|
|
167
|
+
run_reset_admin_password(args)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if __name__ == "__main__":
|
|
171
|
+
main()
|