sharedserver 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.
Files changed (32) hide show
  1. sharedserver-0.1.0/LICENSE +22 -0
  2. sharedserver-0.1.0/MANIFEST.in +2 -0
  3. sharedserver-0.1.0/PKG-INFO +177 -0
  4. sharedserver-0.1.0/README.md +143 -0
  5. sharedserver-0.1.0/pyproject.toml +58 -0
  6. sharedserver-0.1.0/setup.cfg +4 -0
  7. sharedserver-0.1.0/sharedserver.example.yaml +32 -0
  8. sharedserver-0.1.0/src/sharedserver/__init__.py +4 -0
  9. sharedserver-0.1.0/src/sharedserver/__main__.py +6 -0
  10. sharedserver-0.1.0/src/sharedserver/cli.py +76 -0
  11. sharedserver-0.1.0/src/sharedserver/clipboard.py +32 -0
  12. sharedserver-0.1.0/src/sharedserver/config.py +151 -0
  13. sharedserver-0.1.0/src/sharedserver/file_manager.py +131 -0
  14. sharedserver-0.1.0/src/sharedserver/models.py +41 -0
  15. sharedserver-0.1.0/src/sharedserver/security.py +51 -0
  16. sharedserver-0.1.0/src/sharedserver/server.py +142 -0
  17. sharedserver-0.1.0/src/sharedserver/static/app.css +226 -0
  18. sharedserver-0.1.0/src/sharedserver/static/app.js +358 -0
  19. sharedserver-0.1.0/src/sharedserver/templates/index.html +89 -0
  20. sharedserver-0.1.0/src/sharedserver/websocket.py +57 -0
  21. sharedserver-0.1.0/src/sharedserver.egg-info/PKG-INFO +177 -0
  22. sharedserver-0.1.0/src/sharedserver.egg-info/SOURCES.txt +30 -0
  23. sharedserver-0.1.0/src/sharedserver.egg-info/dependency_links.txt +1 -0
  24. sharedserver-0.1.0/src/sharedserver.egg-info/entry_points.txt +2 -0
  25. sharedserver-0.1.0/src/sharedserver.egg-info/requires.txt +10 -0
  26. sharedserver-0.1.0/src/sharedserver.egg-info/top_level.txt +1 -0
  27. sharedserver-0.1.0/tests/test_cli.py +26 -0
  28. sharedserver-0.1.0/tests/test_clipboard.py +44 -0
  29. sharedserver-0.1.0/tests/test_config.py +29 -0
  30. sharedserver-0.1.0/tests/test_project_language.py +29 -0
  31. sharedserver-0.1.0/tests/test_security.py +36 -0
  32. sharedserver-0.1.0/tests/test_server.py +56 -0
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kyi Wong
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.
22
+
@@ -0,0 +1,2 @@
1
+ include sharedserver.example.yaml
2
+
@@ -0,0 +1,177 @@
1
+ Metadata-Version: 2.4
2
+ Name: sharedserver
3
+ Version: 0.1.0
4
+ Summary: Zero-configuration LAN file sharing and real-time text clipboard
5
+ Author: Kyi Wong
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/NihaoKangkang/sharedserver
8
+ Project-URL: Repository, https://github.com/NihaoKangkang/sharedserver
9
+ Project-URL: Issues, https://github.com/NihaoKangkang/sharedserver/issues
10
+ Keywords: file-sharing,clipboard,lan,fastapi,websocket
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Web Environment
13
+ Classifier: Framework :: FastAPI
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: fastapi<1,>=0.115
25
+ Requires-Dist: PyYAML<7,>=6
26
+ Requires-Dist: uvicorn<1,>=0.30
27
+ Requires-Dist: websockets<17,>=13
28
+ Provides-Extra: dev
29
+ Requires-Dist: build<2,>=1.2; extra == "dev"
30
+ Requires-Dist: httpx2<3,>=2; extra == "dev"
31
+ Requires-Dist: pytest<9,>=8; extra == "dev"
32
+ Requires-Dist: twine<7,>=5; extra == "dev"
33
+ Dynamic: license-file
34
+
35
+ # SharedServer
36
+
37
+ SharedServer is a zero-configuration, login-free LAN file server and real-time text clipboard. It is as easy to start as `python -m http.server`, while adding uploads, directory browsing, live text synchronization, and allowlisted clipboard actions.
38
+
39
+ > SharedServer has no authentication. Run it only on a trusted home, lab, or development network. Never expose it directly to the public internet.
40
+
41
+ ## Features
42
+
43
+ - A shared text clipboard synchronized between all open pages over WebSocket
44
+ - A responsive file browser with directory navigation, file sizes, and modification times
45
+ - File uploads from desktop and mobile browsers with live progress
46
+ - Folder creation and safe, non-overwriting uploads
47
+ - Copy actions for allowlisted text and image formats
48
+ - Download-only behavior for other formats such as ZIP, RAR, 7z, EXE, APK, and ISO
49
+ - Protection against path traversal, directory escape, symlink escape, and unsafe filenames
50
+ - No accounts, database, device discovery, template engine, or frontend framework
51
+
52
+ ## Requirements
53
+
54
+ - Python 3.10 or newer
55
+ - A modern desktop or mobile browser
56
+
57
+ ## Installation
58
+
59
+ Install from PyPI:
60
+
61
+ ```bash
62
+ pip install sharedserver
63
+ ```
64
+
65
+ Install from source:
66
+
67
+ ```bash
68
+ git clone https://github.com/NihaoKangkang/sharedserver.git
69
+ cd sharedserver
70
+ pip install .
71
+ ```
72
+
73
+ ## Quick start
74
+
75
+ Run this command from the directory you want to share:
76
+
77
+ ```bash
78
+ python -m sharedserver 8080
79
+ ```
80
+
81
+
82
+ ## CLI reference
83
+
84
+ ```text
85
+ usage: sharedserver [-h] [--host HOST] [-d DIRECTORY] [-c CONFIG]
86
+ [--max-upload-size MB] [--version]
87
+ [port]
88
+ ```
89
+
90
+ | Argument | Description |
91
+ | --- | --- |
92
+ | `port` | Listening port. Uses the configuration value or `8000` by default. |
93
+ | `--host HOST` | Bind address. Defaults to `0.0.0.0`. |
94
+ | `-d, --directory PATH` | Directory to share. Defaults to the current directory. |
95
+ | `-c, --config PATH` | YAML configuration file. |
96
+ | `--max-upload-size MB` | Maximum upload size in MiB. Defaults to 1024 MiB. |
97
+ | `--version` | Print the installed version. |
98
+
99
+ CLI arguments override configuration file values. Without `--config`, SharedServer automatically loads `sharedserver.yaml` from the current directory when it exists.
100
+
101
+ ## Configuration
102
+
103
+ Copy `sharedserver.example.yaml` to `sharedserver.yaml` and edit it as needed:
104
+
105
+ ```yaml
106
+ server:
107
+ host: 0.0.0.0
108
+ port: 8080
109
+
110
+ share:
111
+ directory: ./
112
+ max_upload_size: 1073741824
113
+
114
+ clipboard:
115
+ max_text_size: 2097152
116
+ text_extensions:
117
+ - .txt
118
+ - .md
119
+ image_extensions:
120
+ - .png
121
+ - .jpg
122
+ ```
123
+
124
+ Relative shared directory paths are resolved from the configuration file's directory. Custom extension lists replace the defaults, and matching is case-insensitive.
125
+
126
+ Default text extensions:
127
+
128
+ ```text
129
+ .txt .md .json .xml .yaml .yml .csv .log .py .js .html .css .java .c .cpp
130
+ ```
131
+
132
+ Default image extensions:
133
+
134
+ ```text
135
+ .png .jpg .jpeg .webp
136
+ ```
137
+
138
+ ## Security model
139
+
140
+ - Every user-supplied path is resolved with `Path.resolve()` and checked against the shared root.
141
+ - Absolute paths and `..` path segments are rejected.
142
+ - Symlinks are hidden from directory listings and rejected by file operations.
143
+ - Upload filenames are stripped of path components, control characters, and unsafe cross-platform characters.
144
+ - Uploads are written to a temporary file and published without overwriting an existing file.
145
+ - Both `Content-Length` validation and streamed byte counting enforce the upload limit.
146
+ - Clipboard actions and server read endpoints use the same strict extension allowlists.
147
+ - Browser WebSocket connections from a different origin are rejected.
148
+ - Shared text exists only in process memory and is cleared when the server stops.
149
+
150
+ SharedServer intentionally has no login or authorization layer. Every device that can reach the server can read and write the shared directory and shared text.
151
+
152
+ ## Development
153
+
154
+ Clone the project and create a virtual environment:
155
+
156
+ ```bash
157
+ git clone https://github.com/NihaoKangkang/sharedserver.git
158
+ cd sharedserver
159
+ python -m venv .venv
160
+ ```
161
+
162
+ Activate the environment, then install development dependencies:
163
+
164
+ ```bash
165
+ pip install -e ".[dev]"
166
+ ```
167
+
168
+ Start a development server:
169
+
170
+ ```bash
171
+ python -m sharedserver 8080 --directory .
172
+ ```
173
+
174
+
175
+ ## License
176
+
177
+ [MIT](LICENSE)
@@ -0,0 +1,143 @@
1
+ # SharedServer
2
+
3
+ SharedServer is a zero-configuration, login-free LAN file server and real-time text clipboard. It is as easy to start as `python -m http.server`, while adding uploads, directory browsing, live text synchronization, and allowlisted clipboard actions.
4
+
5
+ > SharedServer has no authentication. Run it only on a trusted home, lab, or development network. Never expose it directly to the public internet.
6
+
7
+ ## Features
8
+
9
+ - A shared text clipboard synchronized between all open pages over WebSocket
10
+ - A responsive file browser with directory navigation, file sizes, and modification times
11
+ - File uploads from desktop and mobile browsers with live progress
12
+ - Folder creation and safe, non-overwriting uploads
13
+ - Copy actions for allowlisted text and image formats
14
+ - Download-only behavior for other formats such as ZIP, RAR, 7z, EXE, APK, and ISO
15
+ - Protection against path traversal, directory escape, symlink escape, and unsafe filenames
16
+ - No accounts, database, device discovery, template engine, or frontend framework
17
+
18
+ ## Requirements
19
+
20
+ - Python 3.10 or newer
21
+ - A modern desktop or mobile browser
22
+
23
+ ## Installation
24
+
25
+ Install from PyPI:
26
+
27
+ ```bash
28
+ pip install sharedserver
29
+ ```
30
+
31
+ Install from source:
32
+
33
+ ```bash
34
+ git clone https://github.com/NihaoKangkang/sharedserver.git
35
+ cd sharedserver
36
+ pip install .
37
+ ```
38
+
39
+ ## Quick start
40
+
41
+ Run this command from the directory you want to share:
42
+
43
+ ```bash
44
+ python -m sharedserver 8080
45
+ ```
46
+
47
+
48
+ ## CLI reference
49
+
50
+ ```text
51
+ usage: sharedserver [-h] [--host HOST] [-d DIRECTORY] [-c CONFIG]
52
+ [--max-upload-size MB] [--version]
53
+ [port]
54
+ ```
55
+
56
+ | Argument | Description |
57
+ | --- | --- |
58
+ | `port` | Listening port. Uses the configuration value or `8000` by default. |
59
+ | `--host HOST` | Bind address. Defaults to `0.0.0.0`. |
60
+ | `-d, --directory PATH` | Directory to share. Defaults to the current directory. |
61
+ | `-c, --config PATH` | YAML configuration file. |
62
+ | `--max-upload-size MB` | Maximum upload size in MiB. Defaults to 1024 MiB. |
63
+ | `--version` | Print the installed version. |
64
+
65
+ CLI arguments override configuration file values. Without `--config`, SharedServer automatically loads `sharedserver.yaml` from the current directory when it exists.
66
+
67
+ ## Configuration
68
+
69
+ Copy `sharedserver.example.yaml` to `sharedserver.yaml` and edit it as needed:
70
+
71
+ ```yaml
72
+ server:
73
+ host: 0.0.0.0
74
+ port: 8080
75
+
76
+ share:
77
+ directory: ./
78
+ max_upload_size: 1073741824
79
+
80
+ clipboard:
81
+ max_text_size: 2097152
82
+ text_extensions:
83
+ - .txt
84
+ - .md
85
+ image_extensions:
86
+ - .png
87
+ - .jpg
88
+ ```
89
+
90
+ Relative shared directory paths are resolved from the configuration file's directory. Custom extension lists replace the defaults, and matching is case-insensitive.
91
+
92
+ Default text extensions:
93
+
94
+ ```text
95
+ .txt .md .json .xml .yaml .yml .csv .log .py .js .html .css .java .c .cpp
96
+ ```
97
+
98
+ Default image extensions:
99
+
100
+ ```text
101
+ .png .jpg .jpeg .webp
102
+ ```
103
+
104
+ ## Security model
105
+
106
+ - Every user-supplied path is resolved with `Path.resolve()` and checked against the shared root.
107
+ - Absolute paths and `..` path segments are rejected.
108
+ - Symlinks are hidden from directory listings and rejected by file operations.
109
+ - Upload filenames are stripped of path components, control characters, and unsafe cross-platform characters.
110
+ - Uploads are written to a temporary file and published without overwriting an existing file.
111
+ - Both `Content-Length` validation and streamed byte counting enforce the upload limit.
112
+ - Clipboard actions and server read endpoints use the same strict extension allowlists.
113
+ - Browser WebSocket connections from a different origin are rejected.
114
+ - Shared text exists only in process memory and is cleared when the server stops.
115
+
116
+ SharedServer intentionally has no login or authorization layer. Every device that can reach the server can read and write the shared directory and shared text.
117
+
118
+ ## Development
119
+
120
+ Clone the project and create a virtual environment:
121
+
122
+ ```bash
123
+ git clone https://github.com/NihaoKangkang/sharedserver.git
124
+ cd sharedserver
125
+ python -m venv .venv
126
+ ```
127
+
128
+ Activate the environment, then install development dependencies:
129
+
130
+ ```bash
131
+ pip install -e ".[dev]"
132
+ ```
133
+
134
+ Start a development server:
135
+
136
+ ```bash
137
+ python -m sharedserver 8080 --directory .
138
+ ```
139
+
140
+
141
+ ## License
142
+
143
+ [MIT](LICENSE)
@@ -0,0 +1,58 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sharedserver"
7
+ version = "0.1.0"
8
+ description = "Zero-configuration LAN file sharing and real-time text clipboard"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Kyi Wong" }]
14
+ keywords = ["file-sharing", "clipboard", "lan", "fastapi", "websocket"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Environment :: Web Environment",
18
+ "Framework :: FastAPI",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Internet :: WWW/HTTP :: HTTP Servers",
26
+ ]
27
+ dependencies = [
28
+ "fastapi>=0.115,<1",
29
+ "PyYAML>=6,<7",
30
+ "uvicorn>=0.30,<1",
31
+ "websockets>=13,<17",
32
+ ]
33
+
34
+ [project.optional-dependencies]
35
+ dev = [
36
+ "build>=1.2,<2",
37
+ "httpx2>=2,<3",
38
+ "pytest>=8,<9",
39
+ "twine>=5,<7",
40
+ ]
41
+
42
+ [project.scripts]
43
+ sharedserver = "sharedserver.cli:main"
44
+
45
+ [project.urls]
46
+ Homepage = "https://github.com/NihaoKangkang/sharedserver"
47
+ Repository = "https://github.com/NihaoKangkang/sharedserver"
48
+ Issues = "https://github.com/NihaoKangkang/sharedserver/issues"
49
+
50
+ [tool.setuptools.packages.find]
51
+ where = ["src"]
52
+
53
+ [tool.setuptools.package-data]
54
+ sharedserver = ["templates/*.html", "static/*.css", "static/*.js"]
55
+
56
+ [tool.pytest.ini_options]
57
+ testpaths = ["tests"]
58
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,32 @@
1
+ server:
2
+ host: 0.0.0.0
3
+ port: 8080
4
+
5
+ share:
6
+ directory: ./
7
+ max_upload_size: 1073741824 # bytes, default 1 GiB
8
+
9
+ clipboard:
10
+ max_text_size: 2097152 # bytes, default 2 MiB
11
+ text_extensions:
12
+ - .txt
13
+ - .md
14
+ - .json
15
+ - .xml
16
+ - .yaml
17
+ - .yml
18
+ - .csv
19
+ - .log
20
+ - .py
21
+ - .js
22
+ - .html
23
+ - .css
24
+ - .java
25
+ - .c
26
+ - .cpp
27
+ image_extensions:
28
+ - .png
29
+ - .jpg
30
+ - .jpeg
31
+ - .webp
32
+
@@ -0,0 +1,4 @@
1
+ """SharedServer package."""
2
+
3
+ __version__ = "0.1.0"
4
+
@@ -0,0 +1,6 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ main()
6
+
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import copy
5
+ import socket
6
+ from pathlib import Path
7
+ from typing import Sequence
8
+
9
+ import uvicorn
10
+
11
+ from . import __version__
12
+ from .config import load_config
13
+ from .server import create_app
14
+
15
+
16
+ def build_parser() -> argparse.ArgumentParser:
17
+ parser = argparse.ArgumentParser(
18
+ prog="sharedserver",
19
+ description="LAN file sharing and real-time text clipboard",
20
+ )
21
+ parser.add_argument("port", nargs="?", type=int, help="port to listen on")
22
+ parser.add_argument("--host", help="host to bind (default: 0.0.0.0)")
23
+ parser.add_argument("-d", "--directory", type=Path, help="directory to share")
24
+ parser.add_argument("-c", "--config", type=Path, help="YAML configuration file")
25
+ parser.add_argument(
26
+ "--max-upload-size",
27
+ type=int,
28
+ metavar="MB",
29
+ help="maximum upload size in MiB",
30
+ )
31
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
32
+ return parser
33
+
34
+
35
+ def get_lan_ip() -> str:
36
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
37
+ try:
38
+ sock.connect(("8.8.8.8", 80))
39
+ return str(sock.getsockname()[0])
40
+ except OSError:
41
+ try:
42
+ return socket.gethostbyname(socket.gethostname())
43
+ except OSError:
44
+ return "127.0.0.1"
45
+ finally:
46
+ sock.close()
47
+
48
+
49
+ def main(argv: Sequence[str] | None = None) -> None:
50
+ parser = build_parser()
51
+ args = parser.parse_args(argv)
52
+ try:
53
+ config = load_config(args.config).with_cli_overrides(
54
+ host=args.host,
55
+ port=args.port,
56
+ directory=args.directory.expanduser().resolve() if args.directory else None,
57
+ max_upload_size=(
58
+ args.max_upload_size * 1024 * 1024
59
+ if args.max_upload_size is not None
60
+ else None
61
+ ),
62
+ )
63
+ app = create_app(config)
64
+ except (FileNotFoundError, NotADirectoryError, ValueError) as exc:
65
+ parser.error(str(exc))
66
+
67
+ port = config.server.port
68
+ print(
69
+ "SharedServer running:\n\n"
70
+ f"Local:\nhttp://127.0.0.1:{port}\n\n"
71
+ f"LAN:\nhttp://{get_lan_ip()}:{port}\n\n"
72
+ f"Sharing: {config.share.directory}\n"
73
+ )
74
+ log_config = copy.deepcopy(uvicorn.config.LOGGING_CONFIG)
75
+ log_config["loggers"]["uvicorn.error"]["level"] = "WARNING"
76
+ uvicorn.run(app, host=config.server.host, port=port, log_config=log_config)
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Literal
5
+
6
+ from .config import ClipboardConfig
7
+
8
+ CopyKind = Literal["text", "image"]
9
+
10
+
11
+ class ClipboardPolicy:
12
+ def __init__(self, config: ClipboardConfig):
13
+ self.text_extensions = frozenset(config.text_extensions)
14
+ self.image_extensions = frozenset(config.image_extensions)
15
+ self.max_text_size = config.max_text_size
16
+
17
+ def copy_kind(self, path: Path) -> CopyKind | None:
18
+ suffix = path.suffix.lower()
19
+ if suffix in self.text_extensions:
20
+ return "text"
21
+ if suffix in self.image_extensions:
22
+ return "image"
23
+ return None
24
+
25
+ def read_text(self, path: Path) -> str:
26
+ if self.copy_kind(path) != "text":
27
+ raise ValueError("file type is not allowed for text copying")
28
+ with path.open("rb") as source:
29
+ content = source.read(self.max_text_size + 1)
30
+ if len(content) > self.max_text_size:
31
+ raise ValueError("text file is too large to copy")
32
+ return content.decode("utf-8-sig", errors="replace")