supernote 0.4.1__tar.gz → 0.6.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.
- {supernote-0.4.1 → supernote-0.6.0}/PKG-INFO +9 -5
- {supernote-0.4.1 → supernote-0.6.0}/README.md +8 -4
- {supernote-0.4.1 → supernote-0.6.0}/pyproject.toml +1 -1
- supernote-0.6.0/supernote/cli/server.py +137 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/server/__init__.py +2 -2
- {supernote-0.4.1 → supernote-0.6.0}/supernote/server/app.py +23 -9
- supernote-0.6.0/supernote/server/config.py +159 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/server/models/file.py +11 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/server/routes/file.py +99 -33
- {supernote-0.4.1 → supernote-0.6.0}/supernote/server/services/file.py +76 -98
- {supernote-0.4.1 → supernote-0.6.0}/supernote/server/services/storage.py +101 -132
- {supernote-0.4.1 → supernote-0.6.0}/supernote/server/services/user.py +61 -66
- {supernote-0.4.1 → supernote-0.6.0}/supernote.egg-info/PKG-INFO +9 -5
- supernote-0.4.1/supernote/cli/server.py +0 -109
- supernote-0.4.1/supernote/server/config.py +0 -16
- {supernote-0.4.1 → supernote-0.6.0}/LICENSE +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/setup.cfg +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/__init__.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/cli/__init__.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/cli/client.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/cli/main.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/cli/notebook.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/client/__init__.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/client/api_model.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/client/auth.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/client/client.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/client/cloud_client.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/client/exceptions.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/client/login_client.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/notebook/__init__.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/notebook/color.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/notebook/converter.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/notebook/decoder.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/notebook/exceptions.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/notebook/fileformat.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/notebook/manipulator.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/notebook/parser.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/notebook/utils.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/py.typed +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/server/models/__init__.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/server/models/auth.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/server/models/base.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/server/routes/auth.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/server/routes/decorators.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/server/routes/system.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote/server/services/__init__.py +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote.egg-info/SOURCES.txt +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote.egg-info/dependency_links.txt +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote.egg-info/entry_points.txt +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote.egg-info/requires.txt +0 -0
- {supernote-0.4.1 → supernote-0.6.0}/supernote.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: supernote
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: All-in-one toolkit for Supernote devices: parse notebooks, self-host services, access services
|
|
5
5
|
Author-email: jya <jya@wizmy.net>, Allen Porter <allen.porter@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -82,8 +82,11 @@ limitation. All credit goes to the original authors of [supernote-tool](https://
|
|
|
82
82
|
### Run Private Server
|
|
83
83
|
|
|
84
84
|
```bash
|
|
85
|
-
#
|
|
86
|
-
supernote-server
|
|
85
|
+
# Generate configuration
|
|
86
|
+
supernote-server config init > config.yaml
|
|
87
|
+
|
|
88
|
+
# Generate users (outputs YAML, append to users.yaml)
|
|
89
|
+
supernote-server user add alice >> users.yaml
|
|
87
90
|
|
|
88
91
|
# Start server
|
|
89
92
|
supernote-server serve
|
|
@@ -95,9 +98,10 @@ supernote-server serve
|
|
|
95
98
|
# Build image
|
|
96
99
|
docker build -t supernote-server .
|
|
97
100
|
|
|
98
|
-
#
|
|
101
|
+
# Generate initial configuration
|
|
99
102
|
mkdir config
|
|
100
|
-
docker run --rm
|
|
103
|
+
docker run --rm supernote-server supernote-server config init > config/config.yaml
|
|
104
|
+
docker run --rm -it supernote-server supernote-server user add alice >> config/users.yaml
|
|
101
105
|
|
|
102
106
|
# Run server
|
|
103
107
|
docker run -d -p 8080:8080 \
|
|
@@ -52,8 +52,11 @@ limitation. All credit goes to the original authors of [supernote-tool](https://
|
|
|
52
52
|
### Run Private Server
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
|
-
#
|
|
56
|
-
supernote-server
|
|
55
|
+
# Generate configuration
|
|
56
|
+
supernote-server config init > config.yaml
|
|
57
|
+
|
|
58
|
+
# Generate users (outputs YAML, append to users.yaml)
|
|
59
|
+
supernote-server user add alice >> users.yaml
|
|
57
60
|
|
|
58
61
|
# Start server
|
|
59
62
|
supernote-server serve
|
|
@@ -65,9 +68,10 @@ supernote-server serve
|
|
|
65
68
|
# Build image
|
|
66
69
|
docker build -t supernote-server .
|
|
67
70
|
|
|
68
|
-
#
|
|
71
|
+
# Generate initial configuration
|
|
69
72
|
mkdir config
|
|
70
|
-
docker run --rm
|
|
73
|
+
docker run --rm supernote-server supernote-server config init > config/config.yaml
|
|
74
|
+
docker run --rm -it supernote-server supernote-server user add alice >> config/users.yaml
|
|
71
75
|
|
|
72
76
|
# Run server
|
|
73
77
|
docker run -d -p 8080:8080 \
|
|
@@ -4,7 +4,7 @@ requires = ["setuptools>=77.0"]
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "supernote"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.0"
|
|
8
8
|
license = "Apache-2.0"
|
|
9
9
|
license-files = ["LICENSE"]
|
|
10
10
|
description = "All-in-one toolkit for Supernote devices: parse notebooks, self-host services, access services"
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Server CLI commands."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import getpass
|
|
5
|
+
import secrets
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from supernote.server import app as server_app
|
|
10
|
+
from supernote.server.config import AuthConfig, ServerConfig, UsersConfig
|
|
11
|
+
from supernote.server.services.user import UserService
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_auth_config(config_dir: str | None) -> AuthConfig:
|
|
15
|
+
# Load base config
|
|
16
|
+
server_config = ServerConfig.load(config_dir)
|
|
17
|
+
return server_config.auth
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def list_users(config_dir: str | None):
|
|
21
|
+
config = get_auth_config(config_dir)
|
|
22
|
+
service = UserService(config)
|
|
23
|
+
for user in service.list_users():
|
|
24
|
+
status = "active" if user.is_active else "inactive"
|
|
25
|
+
print(f"{user.username} ({status})")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def add_user(username: str, password: str | None = None):
|
|
29
|
+
if password is None:
|
|
30
|
+
password = getpass.getpass(f"Password for {username}: ")
|
|
31
|
+
|
|
32
|
+
# Generate the YAML snippet for a single user
|
|
33
|
+
user_entry = UserService.create_user_entry(username, password)
|
|
34
|
+
users_config = UsersConfig(users=[user_entry])
|
|
35
|
+
|
|
36
|
+
# Print YAML to stdout
|
|
37
|
+
print("# Generated Supernote User Entry")
|
|
38
|
+
print("# Append this to your users.yaml file.")
|
|
39
|
+
print(yaml.safe_dump(users_config.to_dict(), default_flow_style=False))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def config_init():
|
|
43
|
+
"""Output a default config.yaml with a generated secret."""
|
|
44
|
+
secret = secrets.token_hex(32)
|
|
45
|
+
config = ServerConfig()
|
|
46
|
+
config.auth.secret_key = secret
|
|
47
|
+
|
|
48
|
+
print("# Generated Supernote Server Configuration")
|
|
49
|
+
print("# Save this as config.yaml.")
|
|
50
|
+
print(yaml.safe_dump(config.to_dict(), default_flow_style=False))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def generate_secret():
|
|
54
|
+
"""Output a 32-byte random hex secret."""
|
|
55
|
+
print(secrets.token_hex(32))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def add_parser(subparsers):
|
|
59
|
+
# Common parent parser for server-related commands that need config access
|
|
60
|
+
base_parser = argparse.ArgumentParser(add_help=False)
|
|
61
|
+
base_parser.add_argument(
|
|
62
|
+
"--config-dir",
|
|
63
|
+
type=str,
|
|
64
|
+
default=None,
|
|
65
|
+
help="Path to configuration directory (default: config/)",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# 'serve' subcommand
|
|
69
|
+
parser_serve = subparsers.add_parser(
|
|
70
|
+
"serve",
|
|
71
|
+
parents=[base_parser],
|
|
72
|
+
help="Start the Supernote Private Cloud server",
|
|
73
|
+
)
|
|
74
|
+
parser_serve.set_defaults(func=server_app.run)
|
|
75
|
+
|
|
76
|
+
# User management
|
|
77
|
+
parser_user = subparsers.add_parser("user", help="User management commands")
|
|
78
|
+
user_subparsers = parser_user.add_subparsers(dest="user_command")
|
|
79
|
+
|
|
80
|
+
# user list
|
|
81
|
+
parser_user_list = user_subparsers.add_parser(
|
|
82
|
+
"list",
|
|
83
|
+
parents=[base_parser],
|
|
84
|
+
help="List all users in the current config file",
|
|
85
|
+
)
|
|
86
|
+
parser_user_list.set_defaults(
|
|
87
|
+
func=lambda args: list_users(getattr(args, "config_dir", None))
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# user add
|
|
91
|
+
parser_user_add = user_subparsers.add_parser(
|
|
92
|
+
"add",
|
|
93
|
+
parents=[base_parser],
|
|
94
|
+
help="Generate a user configuration snippet",
|
|
95
|
+
)
|
|
96
|
+
parser_user_add.add_argument("username", type=str, help="Username for the new user")
|
|
97
|
+
parser_user_add.add_argument(
|
|
98
|
+
"--password", type=str, help="Password (if omitted, prompt interactively)"
|
|
99
|
+
)
|
|
100
|
+
parser_user_add.set_defaults(
|
|
101
|
+
func=lambda args: add_user(args.username, args.password)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# config group
|
|
105
|
+
parser_config = subparsers.add_parser("config", help="Configuration utilities")
|
|
106
|
+
config_subparsers = parser_config.add_subparsers(dest="config_command")
|
|
107
|
+
|
|
108
|
+
# config init
|
|
109
|
+
parser_config_init = config_subparsers.add_parser(
|
|
110
|
+
"init",
|
|
111
|
+
parents=[base_parser],
|
|
112
|
+
help="Generate a default config.yaml",
|
|
113
|
+
)
|
|
114
|
+
parser_config_init.set_defaults(func=lambda _: config_init())
|
|
115
|
+
|
|
116
|
+
# config secret
|
|
117
|
+
parser_config_secret = config_subparsers.add_parser(
|
|
118
|
+
"generate-secret",
|
|
119
|
+
parents=[base_parser],
|
|
120
|
+
help="Generate a secure random secret",
|
|
121
|
+
)
|
|
122
|
+
parser_config_secret.set_defaults(func=lambda _: generate_secret())
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def main():
|
|
126
|
+
parser = argparse.ArgumentParser(description="Supernote Server CLI")
|
|
127
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
128
|
+
add_parser(subparsers)
|
|
129
|
+
args = parser.parse_args()
|
|
130
|
+
if hasattr(args, "func"):
|
|
131
|
+
args.func(args)
|
|
132
|
+
else:
|
|
133
|
+
parser.print_help()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__":
|
|
137
|
+
main()
|
|
@@ -6,9 +6,9 @@ from typing import Any, Awaitable, Callable
|
|
|
6
6
|
|
|
7
7
|
from aiohttp import web
|
|
8
8
|
|
|
9
|
-
from supernote.server.services.user import JWT_ALGORITHM
|
|
9
|
+
from supernote.server.services.user import JWT_ALGORITHM
|
|
10
10
|
|
|
11
|
-
from . import
|
|
11
|
+
from .config import ServerConfig
|
|
12
12
|
from .models.base import create_error_response
|
|
13
13
|
from .routes import auth, file, system
|
|
14
14
|
from .services.file import FileService
|
|
@@ -56,8 +56,11 @@ async def trace_middleware(
|
|
|
56
56
|
"body": body_str,
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
# Get config from app
|
|
60
|
+
server_config: ServerConfig = request.app["config"]
|
|
61
|
+
|
|
59
62
|
try:
|
|
60
|
-
with open(
|
|
63
|
+
with open(server_config.trace_log_file, "a") as f:
|
|
61
64
|
f.write(json.dumps(log_entry) + "\n")
|
|
62
65
|
f.flush()
|
|
63
66
|
except Exception as e:
|
|
@@ -92,8 +95,12 @@ async def jwt_auth_middleware(
|
|
|
92
95
|
|
|
93
96
|
import jwt
|
|
94
97
|
|
|
98
|
+
server_config: ServerConfig = request.app["config"]
|
|
99
|
+
|
|
95
100
|
try:
|
|
96
|
-
payload = jwt.decode(
|
|
101
|
+
payload = jwt.decode(
|
|
102
|
+
token, server_config.auth.secret_key, algorithms=[JWT_ALGORITHM]
|
|
103
|
+
)
|
|
97
104
|
except jwt.InvalidTokenError:
|
|
98
105
|
return web.json_response(
|
|
99
106
|
create_error_response("Invalid token").to_dict(), status=401
|
|
@@ -102,16 +109,21 @@ async def jwt_auth_middleware(
|
|
|
102
109
|
return await handler(request)
|
|
103
110
|
|
|
104
111
|
|
|
105
|
-
def create_app() -> web.Application:
|
|
112
|
+
def create_app(config: ServerConfig | None = None) -> web.Application:
|
|
113
|
+
if config is None:
|
|
114
|
+
config = ServerConfig.load()
|
|
115
|
+
|
|
106
116
|
app = web.Application(middlewares=[trace_middleware, jwt_auth_middleware])
|
|
117
|
+
app["config"] = config
|
|
107
118
|
|
|
108
119
|
# Initialize services
|
|
109
|
-
storage_root = Path(config.
|
|
120
|
+
storage_root = Path(config.storage_dir)
|
|
110
121
|
temp_root = storage_root / "temp"
|
|
111
122
|
storage_service = StorageService(storage_root, temp_root)
|
|
112
123
|
app["storage_service"] = storage_service
|
|
113
|
-
app["user_service"] = UserService(config.
|
|
124
|
+
app["user_service"] = UserService(config.auth)
|
|
114
125
|
app["file_service"] = FileService(storage_service)
|
|
126
|
+
app["sync_locks"] = {} # user -> (equipment_no, expiry_time)
|
|
115
127
|
|
|
116
128
|
# Register routes
|
|
117
129
|
app.add_routes(system.routes)
|
|
@@ -125,5 +137,7 @@ def create_app() -> web.Application:
|
|
|
125
137
|
|
|
126
138
|
def run(args: Any) -> None:
|
|
127
139
|
logging.basicConfig(level=logging.DEBUG)
|
|
128
|
-
|
|
129
|
-
|
|
140
|
+
config_dir = getattr(args, "config_dir", None)
|
|
141
|
+
config = ServerConfig.load(config_dir)
|
|
142
|
+
app = create_app(config)
|
|
143
|
+
web.run_app(app, host=config.host, port=config.port)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import secrets
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
from mashumaro.mixins.dict import DataClassDictMixin
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class AuthConfig(DataClassDictMixin):
|
|
16
|
+
secret_key: str = ""
|
|
17
|
+
expiration_hours: int = 24
|
|
18
|
+
users_file: str = "users.yaml"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class UserEntry(DataClassDictMixin):
|
|
23
|
+
username: str
|
|
24
|
+
password_md5: str
|
|
25
|
+
is_active: bool = True
|
|
26
|
+
devices: list[str] = field(default_factory=list)
|
|
27
|
+
profile: dict[str, Any] = field(default_factory=dict)
|
|
28
|
+
mobile: str | None = None
|
|
29
|
+
email: str | None = None
|
|
30
|
+
avatar: str | None = None
|
|
31
|
+
signature: str | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class UsersConfig(DataClassDictMixin):
|
|
36
|
+
users: list[UserEntry] = field(default_factory=list)
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def load(cls, path: str | Path) -> "UsersConfig":
|
|
40
|
+
path = Path(path)
|
|
41
|
+
if not path.exists():
|
|
42
|
+
return cls()
|
|
43
|
+
try:
|
|
44
|
+
with open(path, "r") as f:
|
|
45
|
+
data = yaml.safe_load(f) or {}
|
|
46
|
+
return cls.from_dict(data)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.warning(f"Failed to load users file {path}: {e}")
|
|
49
|
+
return cls()
|
|
50
|
+
|
|
51
|
+
def save(self, path: str | Path) -> None:
|
|
52
|
+
path = Path(path)
|
|
53
|
+
if not path.parent.exists():
|
|
54
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
with open(path, "w") as f:
|
|
56
|
+
yaml.safe_dump(self.to_dict(), f, default_flow_style=False)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class ServerConfig(DataClassDictMixin):
|
|
61
|
+
host: str = "0.0.0.0"
|
|
62
|
+
port: int = 8080
|
|
63
|
+
trace_log_file: str = "data/server_trace.log"
|
|
64
|
+
storage_dir: str = "storage"
|
|
65
|
+
auth: AuthConfig = field(default_factory=AuthConfig)
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def load(cls, config_dir: str | Path | None = None) -> "ServerConfig":
|
|
69
|
+
"""Load configuration from directory."""
|
|
70
|
+
# 1. Determine config directory
|
|
71
|
+
if config_dir is None:
|
|
72
|
+
config_dir = os.getenv("SUPERNOTE_CONFIG_DIR", "config")
|
|
73
|
+
|
|
74
|
+
config_dir_path = Path(config_dir)
|
|
75
|
+
config_file = config_dir_path / "config.yaml"
|
|
76
|
+
|
|
77
|
+
# 2. Load from YAML if exists
|
|
78
|
+
file_data: dict[str, Any] = {}
|
|
79
|
+
if file_exists := config_file.exists():
|
|
80
|
+
try:
|
|
81
|
+
with open(config_file, "r") as f:
|
|
82
|
+
file_data = yaml.safe_load(f) or {}
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.warning(f"Failed to load config file {config_file}: {e}")
|
|
85
|
+
|
|
86
|
+
# 3. Create config object using mashumaro
|
|
87
|
+
try:
|
|
88
|
+
config = cls.from_dict(file_data)
|
|
89
|
+
except Exception as e:
|
|
90
|
+
logger.warning(
|
|
91
|
+
f"Failed to parse config file {config_file}, using defaults: {e}"
|
|
92
|
+
)
|
|
93
|
+
config = cls()
|
|
94
|
+
|
|
95
|
+
# Resolve users_file relative to config_dir if it's just a filename (not already a path)
|
|
96
|
+
users_path = Path(config.auth.users_file)
|
|
97
|
+
if not users_path.is_absolute() and len(users_path.parts) == 1:
|
|
98
|
+
config.auth.users_file = str(config_dir_path / users_path)
|
|
99
|
+
|
|
100
|
+
# 4. Generate secret if missing AND not provided by env var
|
|
101
|
+
env_secret = os.getenv("SUPERNOTE_JWT_SECRET")
|
|
102
|
+
secret_generated = False
|
|
103
|
+
|
|
104
|
+
if not config.auth.secret_key and not env_secret:
|
|
105
|
+
logger.warning("No JWT secret key configured. Generating a new random key.")
|
|
106
|
+
config.auth.secret_key = secrets.token_hex(32)
|
|
107
|
+
secret_generated = True
|
|
108
|
+
|
|
109
|
+
# 5. Save config if it was missing or if we generated a secret
|
|
110
|
+
if not file_exists or secret_generated:
|
|
111
|
+
try:
|
|
112
|
+
# Ensure directory exists
|
|
113
|
+
if not config_file.parent.exists():
|
|
114
|
+
config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
115
|
+
|
|
116
|
+
# Prepare config for saving (restore relative paths if possible)
|
|
117
|
+
config_to_save = cls.from_dict(config.to_dict())
|
|
118
|
+
try:
|
|
119
|
+
users_path_abs = Path(config.auth.users_file)
|
|
120
|
+
if users_path_abs.is_absolute() and users_path_abs.is_relative_to(
|
|
121
|
+
config_dir_path
|
|
122
|
+
):
|
|
123
|
+
config_to_save.auth.users_file = str(
|
|
124
|
+
users_path_abs.relative_to(config_dir_path)
|
|
125
|
+
)
|
|
126
|
+
except ValueError:
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
with open(config_file, "w") as f:
|
|
130
|
+
yaml.safe_dump(
|
|
131
|
+
config_to_save.to_dict(), f, default_flow_style=False
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if secret_generated:
|
|
135
|
+
logger.info(
|
|
136
|
+
f"Saved new configuration with generated secret to {config_file}"
|
|
137
|
+
)
|
|
138
|
+
else:
|
|
139
|
+
logger.info(f"Created default configuration file at {config_file}")
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.warning(f"Failed to save configuration to {config_file}: {e}")
|
|
142
|
+
|
|
143
|
+
# 6. Apply env var override (runtime only, not saved)
|
|
144
|
+
if env_secret:
|
|
145
|
+
config.auth.secret_key = env_secret
|
|
146
|
+
|
|
147
|
+
if os.getenv("SUPERNOTE_HOST"):
|
|
148
|
+
config.host = os.getenv("SUPERNOTE_HOST", config.host)
|
|
149
|
+
|
|
150
|
+
if os.getenv("SUPERNOTE_PORT"):
|
|
151
|
+
try:
|
|
152
|
+
config.port = int(os.getenv("SUPERNOTE_PORT", str(config.port)))
|
|
153
|
+
except ValueError:
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
if os.getenv("SUPERNOTE_STORAGE_DIR"):
|
|
157
|
+
config.storage_dir = os.getenv("SUPERNOTE_STORAGE_DIR", config.storage_dir)
|
|
158
|
+
|
|
159
|
+
return config
|
|
@@ -25,6 +25,17 @@ class SyncStartRequest(DataClassJSONMixin):
|
|
|
25
25
|
code_generation_options = [TO_DICT_ADD_OMIT_NONE_FLAG] # type: ignore[list-item]
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
@dataclass
|
|
29
|
+
class SyncEndRequest(DataClassJSONMixin):
|
|
30
|
+
equipment_no: str = field(metadata=field_options(alias="equipmentNo"))
|
|
31
|
+
flag: str | None = None # "true" or "false"
|
|
32
|
+
|
|
33
|
+
class Config(BaseConfig):
|
|
34
|
+
serialize_by_alias = True
|
|
35
|
+
omit_none = True
|
|
36
|
+
code_generation_options = [TO_DICT_ADD_OMIT_NONE_FLAG] # type: ignore[list-item]
|
|
37
|
+
|
|
38
|
+
|
|
28
39
|
@dataclass
|
|
29
40
|
class ListFolderRequest(DataClassJSONMixin):
|
|
30
41
|
path: str = "/"
|