cista 0.7.2__tar.gz → 1.0.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 (146) hide show
  1. {cista-0.7.2 → cista-1.0.0}/.gitignore +1 -0
  2. {cista-0.7.2 → cista-1.0.0}/PKG-INFO +85 -61
  3. {cista-0.7.2 → cista-1.0.0}/README.md +46 -41
  4. {cista-0.7.2 → cista-1.0.0}/cista/_version.py +1 -1
  5. cista-1.0.0/cista/preview.py +225 -0
  6. {cista-0.7.2 → cista-1.0.0}/cista/protocol.py +1 -2
  7. {cista-0.7.2 → cista-1.0.0}/cista/serve.py +9 -0
  8. {cista-0.7.2 → cista-1.0.0}/cista/util/apphelpers.py +2 -0
  9. {cista-0.7.2 → cista-1.0.0}/cista/watching.py +99 -36
  10. cista-1.0.0/cista/wwwroot/assets/add-file-6a41e251.js +1 -0
  11. cista-1.0.0/cista/wwwroot/assets/add-folder-62d934ef.js +1 -0
  12. cista-1.0.0/cista/wwwroot/assets/arrow-a3a583f8.js +1 -0
  13. cista-1.0.0/cista/wwwroot/assets/arrows-h-d9e36082.js +1 -0
  14. cista-1.0.0/cista/wwwroot/assets/arrows-v-5223491a.js +1 -0
  15. cista-1.0.0/cista/wwwroot/assets/check-f7566407.js +1 -0
  16. cista-1.0.0/cista/wwwroot/assets/code-8e0123d4.js +1 -0
  17. cista-1.0.0/cista/wwwroot/assets/copy-a70a3d1c.js +1 -0
  18. cista-1.0.0/cista/wwwroot/assets/create-file-d995875e.js +1 -0
  19. cista-1.0.0/cista/wwwroot/assets/create-folder-c448deda.js +1 -0
  20. cista-1.0.0/cista/wwwroot/assets/cross-e74ed6f4.js +1 -0
  21. cista-1.0.0/cista/wwwroot/assets/disk-f1376321.js +1 -0
  22. cista-1.0.0/cista/wwwroot/assets/download-bad67930.js +1 -0
  23. cista-1.0.0/cista/wwwroot/assets/exclamation-afec06bb.js +1 -0
  24. cista-1.0.0/cista/wwwroot/assets/eye-5fde34a1.js +1 -0
  25. cista-1.0.0/cista/wwwroot/assets/find-11711648.js +1 -0
  26. cista-1.0.0/cista/wwwroot/assets/fullscreen-7f61e522.js +1 -0
  27. cista-1.0.0/cista/wwwroot/assets/github-c9475017.js +1 -0
  28. cista-1.0.0/cista/wwwroot/assets/index-0ccc4ea2.js +28 -0
  29. cista-1.0.0/cista/wwwroot/assets/index-89d850f4.css +1 -0
  30. cista-1.0.0/cista/wwwroot/assets/info-b3d76e57.js +1 -0
  31. cista-1.0.0/cista/wwwroot/assets/link-5cd893e4.js +1 -0
  32. cista-1.0.0/cista/wwwroot/assets/logo-6090454d.js +1 -0
  33. cista-1.0.0/cista/wwwroot/assets/loop-531f6994.js +1 -0
  34. cista-1.0.0/cista/wwwroot/assets/menu-ed0d8c47.js +1 -0
  35. cista-1.0.0/cista/wwwroot/assets/next-ee82241a.js +1 -0
  36. cista-1.0.0/cista/wwwroot/assets/open-8364df82.js +1 -0
  37. cista-1.0.0/cista/wwwroot/assets/paste-0f86e193.js +1 -0
  38. cista-1.0.0/cista/wwwroot/assets/pause-a2dd4670.js +1 -0
  39. cista-1.0.0/cista/wwwroot/assets/pencil-bfd02151.js +1 -0
  40. cista-1.0.0/cista/wwwroot/assets/play-83e40a03.js +1 -0
  41. cista-1.0.0/cista/wwwroot/assets/plus-f463cfbc.js +1 -0
  42. cista-1.0.0/cista/wwwroot/assets/previous-cbb778b5.js +1 -0
  43. cista-1.0.0/cista/wwwroot/assets/reload-907eb866.js +1 -0
  44. cista-1.0.0/cista/wwwroot/assets/rename-51dadde6.js +1 -0
  45. cista-1.0.0/cista/wwwroot/assets/scissors-d32350cc.js +1 -0
  46. cista-1.0.0/cista/wwwroot/assets/shuffle-00a003ad.js +1 -0
  47. cista-1.0.0/cista/wwwroot/assets/signin-d7bd57fd.js +1 -0
  48. cista-1.0.0/cista/wwwroot/assets/signout-4c4ff7fb.js +1 -0
  49. cista-1.0.0/cista/wwwroot/assets/skip-0069d8d7.js +1 -0
  50. cista-1.0.0/cista/wwwroot/assets/spinner-164c9b34.js +1 -0
  51. cista-1.0.0/cista/wwwroot/assets/stop-3717c27d.js +1 -0
  52. cista-1.0.0/cista/wwwroot/assets/trash-449f81ef.js +1 -0
  53. cista-1.0.0/cista/wwwroot/assets/triangle-3ecaf29c.js +1 -0
  54. cista-1.0.0/cista/wwwroot/assets/unfullscreen-175d46cd.js +1 -0
  55. cista-1.0.0/cista/wwwroot/assets/up-arrow-164a366e.js +1 -0
  56. cista-1.0.0/cista/wwwroot/assets/upload-cloud-df377f1a.js +1 -0
  57. cista-1.0.0/cista/wwwroot/assets/user-3b5a32bc.js +1 -0
  58. cista-1.0.0/cista/wwwroot/assets/user-cog-27e2c201.js +1 -0
  59. cista-1.0.0/cista/wwwroot/assets/volume-high-f4e07edd.js +1 -0
  60. cista-1.0.0/cista/wwwroot/assets/volume-low-f754322d.js +1 -0
  61. cista-1.0.0/cista/wwwroot/assets/volume-medium-a5806088.js +1 -0
  62. cista-1.0.0/cista/wwwroot/assets/volume-mute-405763b1.js +1 -0
  63. cista-1.0.0/cista/wwwroot/assets/window-cross-86cb8236.js +1 -0
  64. cista-1.0.0/cista/wwwroot/assets/window-d99968ac.js +1 -0
  65. cista-1.0.0/cista/wwwroot/assets/wordwrap-cdcd3f93.js +1 -0
  66. cista-1.0.0/cista/wwwroot/assets/zoomin-7d13bb9b.js +1 -0
  67. cista-1.0.0/cista/wwwroot/assets/zoomout-daf52018.js +1 -0
  68. {cista-0.7.2 → cista-1.0.0}/cista/wwwroot/index.html +2 -2
  69. cista-1.0.0/pyproject.toml +154 -0
  70. cista-0.7.2/cista/preview.py +0 -117
  71. cista-0.7.2/cista/wwwroot/assets/add-file-0ff53f2c.js +0 -1
  72. cista-0.7.2/cista/wwwroot/assets/add-folder-0ac641fd.js +0 -1
  73. cista-0.7.2/cista/wwwroot/assets/arrow-6d57743c.js +0 -1
  74. cista-0.7.2/cista/wwwroot/assets/arrows-h-9715a1dc.js +0 -1
  75. cista-0.7.2/cista/wwwroot/assets/arrows-v-17478ebc.js +0 -1
  76. cista-0.7.2/cista/wwwroot/assets/check-b37d535a.js +0 -1
  77. cista-0.7.2/cista/wwwroot/assets/code-35dab0d8.js +0 -1
  78. cista-0.7.2/cista/wwwroot/assets/copy-08cc120b.js +0 -1
  79. cista-0.7.2/cista/wwwroot/assets/create-file-48e89b04.js +0 -1
  80. cista-0.7.2/cista/wwwroot/assets/create-folder-b9d2fd99.js +0 -1
  81. cista-0.7.2/cista/wwwroot/assets/cross-9008d2a4.js +0 -1
  82. cista-0.7.2/cista/wwwroot/assets/disk-5e08f410.js +0 -1
  83. cista-0.7.2/cista/wwwroot/assets/download-72de2989.js +0 -1
  84. cista-0.7.2/cista/wwwroot/assets/exclamation-a7d9f408.js +0 -1
  85. cista-0.7.2/cista/wwwroot/assets/eye-73e4e6be.js +0 -1
  86. cista-0.7.2/cista/wwwroot/assets/find-ec0a7765.js +0 -1
  87. cista-0.7.2/cista/wwwroot/assets/fullscreen-7cf77f36.js +0 -1
  88. cista-0.7.2/cista/wwwroot/assets/github-768c474d.js +0 -1
  89. cista-0.7.2/cista/wwwroot/assets/index-1332f321.js +0 -13
  90. cista-0.7.2/cista/wwwroot/assets/index-456bb401.css +0 -1
  91. cista-0.7.2/cista/wwwroot/assets/info-c28d09ff.js +0 -1
  92. cista-0.7.2/cista/wwwroot/assets/link-8cf3bdf0.js +0 -1
  93. cista-0.7.2/cista/wwwroot/assets/logo-d5f794ec.js +0 -1
  94. cista-0.7.2/cista/wwwroot/assets/loop-fedc4169.js +0 -1
  95. cista-0.7.2/cista/wwwroot/assets/menu-0c34e3e2.js +0 -1
  96. cista-0.7.2/cista/wwwroot/assets/next-0a58f144.js +0 -1
  97. cista-0.7.2/cista/wwwroot/assets/open-35afd848.js +0 -1
  98. cista-0.7.2/cista/wwwroot/assets/paste-e0ef001a.js +0 -1
  99. cista-0.7.2/cista/wwwroot/assets/pause-a35acf21.js +0 -1
  100. cista-0.7.2/cista/wwwroot/assets/pencil-cd90f93a.js +0 -1
  101. cista-0.7.2/cista/wwwroot/assets/play-f535c62d.js +0 -1
  102. cista-0.7.2/cista/wwwroot/assets/plus-b2df8058.js +0 -1
  103. cista-0.7.2/cista/wwwroot/assets/previous-85fd5e4d.js +0 -1
  104. cista-0.7.2/cista/wwwroot/assets/reload-e27d8eda.js +0 -1
  105. cista-0.7.2/cista/wwwroot/assets/rename-a6cb8477.js +0 -1
  106. cista-0.7.2/cista/wwwroot/assets/scissors-c98a2f01.js +0 -1
  107. cista-0.7.2/cista/wwwroot/assets/shuffle-07c5c925.js +0 -1
  108. cista-0.7.2/cista/wwwroot/assets/signin-c2a74424.js +0 -1
  109. cista-0.7.2/cista/wwwroot/assets/signout-ce13f56f.js +0 -1
  110. cista-0.7.2/cista/wwwroot/assets/skip-81664c53.js +0 -1
  111. cista-0.7.2/cista/wwwroot/assets/spinner-95826db5.js +0 -1
  112. cista-0.7.2/cista/wwwroot/assets/stop-acb41526.js +0 -1
  113. cista-0.7.2/cista/wwwroot/assets/trash-c8e2fdaa.js +0 -1
  114. cista-0.7.2/cista/wwwroot/assets/triangle-c25a1114.js +0 -1
  115. cista-0.7.2/cista/wwwroot/assets/unfullscreen-e7cc25a1.js +0 -1
  116. cista-0.7.2/cista/wwwroot/assets/up-arrow-a110006a.js +0 -1
  117. cista-0.7.2/cista/wwwroot/assets/upload-cloud-6cdf142b.js +0 -1
  118. cista-0.7.2/cista/wwwroot/assets/user-2d648d32.js +0 -1
  119. cista-0.7.2/cista/wwwroot/assets/user-cog-a90f18ca.js +0 -1
  120. cista-0.7.2/cista/wwwroot/assets/volume-high-792bbac7.js +0 -1
  121. cista-0.7.2/cista/wwwroot/assets/volume-low-d40f717a.js +0 -1
  122. cista-0.7.2/cista/wwwroot/assets/volume-medium-073ff083.js +0 -1
  123. cista-0.7.2/cista/wwwroot/assets/volume-mute-ed5cdbbb.js +0 -1
  124. cista-0.7.2/cista/wwwroot/assets/window-4bf01d12.js +0 -1
  125. cista-0.7.2/cista/wwwroot/assets/window-cross-d62113fd.js +0 -1
  126. cista-0.7.2/cista/wwwroot/assets/wordwrap-1032ac82.js +0 -1
  127. cista-0.7.2/cista/wwwroot/assets/zoomin-98208790.js +0 -1
  128. cista-0.7.2/cista/wwwroot/assets/zoomout-ea6810cf.js +0 -1
  129. cista-0.7.2/pyproject.toml +0 -105
  130. {cista-0.7.2 → cista-1.0.0}/cista/__init__.py +0 -0
  131. {cista-0.7.2 → cista-1.0.0}/cista/__main__.py +0 -0
  132. {cista-0.7.2 → cista-1.0.0}/cista/api.py +0 -0
  133. {cista-0.7.2 → cista-1.0.0}/cista/app.py +0 -0
  134. {cista-0.7.2 → cista-1.0.0}/cista/auth.py +0 -0
  135. {cista-0.7.2 → cista-1.0.0}/cista/config.py +0 -0
  136. {cista-0.7.2 → cista-1.0.0}/cista/droppy.py +0 -0
  137. {cista-0.7.2 → cista-1.0.0}/cista/fileio.py +0 -0
  138. {cista-0.7.2 → cista-1.0.0}/cista/server80.py +0 -0
  139. {cista-0.7.2 → cista-1.0.0}/cista/session.py +0 -0
  140. {cista-0.7.2 → cista-1.0.0}/cista/util/__init__.py +0 -0
  141. {cista-0.7.2 → cista-1.0.0}/cista/util/asynclink.py +0 -0
  142. {cista-0.7.2 → cista-1.0.0}/cista/util/filename.py +0 -0
  143. {cista-0.7.2 → cista-1.0.0}/cista/util/lrucache.py +0 -0
  144. {cista-0.7.2 → cista-1.0.0}/cista/util/pwgen.py +0 -0
  145. {cista-0.7.2 → cista-1.0.0}/cista/wwwroot/assets/logo-97d1d7eb.svg +0 -0
  146. {cista-0.7.2 → cista-1.0.0}/cista/wwwroot/robots.txt +0 -0
@@ -1,4 +1,5 @@
1
1
  .*
2
+ *.lock
2
3
  !.gitignore
3
4
  __pycache__/
4
5
  *.egg-info/
@@ -1,29 +1,48 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: cista
3
- Version: 0.7.2
3
+ Version: 1.0.0
4
4
  Summary: Dropbox-like file server with modern web interface
5
5
  Project-URL: Homepage, https://git.zi.fi/Vasanko/cista-storage
6
6
  Author: Vasanko
7
+ Maintainer: Vasanko
8
+ Keywords: dropbox,file-server,storage,web-interface
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Environment :: Web Environment
11
+ Classifier: Intended Audience :: End Users/Desktop
12
+ Classifier: Intended Audience :: System Administrators
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: License :: Public Domain
7
15
  Requires-Python: >=3.11
8
- Requires-Dist: argon2-cffi
9
- Requires-Dist: blake3
10
- Requires-Dist: brotli
11
- Requires-Dist: docopt
12
- Requires-Dist: inotify
13
- Requires-Dist: msgspec
14
- Requires-Dist: natsort
15
- Requires-Dist: pathvalidate
16
- Requires-Dist: pillow
17
- Requires-Dist: pyav
18
- Requires-Dist: pyjwt
19
- Requires-Dist: pymupdf
20
- Requires-Dist: sanic
21
- Requires-Dist: setproctitle
22
- Requires-Dist: stream-zip
23
- Requires-Dist: tomli-w
16
+ Requires-Dist: argon2-cffi>=25.1.0
17
+ Requires-Dist: av>=15.0.0
18
+ Requires-Dist: blake3>=1.0.5
19
+ Requires-Dist: brotli>=1.1.0
20
+ Requires-Dist: docopt>=0.6.2
21
+ Requires-Dist: inotify>=0.2.12
22
+ Requires-Dist: msgspec>=0.19.0
23
+ Requires-Dist: natsort>=8.4.0
24
+ Requires-Dist: numpy>=2.3.2
25
+ Requires-Dist: pathvalidate>=3.3.1
26
+ Requires-Dist: pillow-heif>=1.1.0
27
+ Requires-Dist: pillow>=11.3.0
28
+ Requires-Dist: pyjwt>=2.10.1
29
+ Requires-Dist: pymupdf>=1.26.3
30
+ Requires-Dist: sanic>=25.3.0
31
+ Requires-Dist: setproctitle>=1.3.6
32
+ Requires-Dist: stream-zip>=0.0.83
33
+ Requires-Dist: tomli-w>=1.2.0
24
34
  Provides-Extra: dev
25
- Requires-Dist: pytest; extra == 'dev'
26
- Requires-Dist: ruff; extra == 'dev'
35
+ Requires-Dist: mypy>=1.13.0; extra == 'dev'
36
+ Requires-Dist: pre-commit>=4.0.0; extra == 'dev'
37
+ Requires-Dist: pytest>=8.4.1; extra == 'dev'
38
+ Requires-Dist: ruff>=0.8.0; extra == 'dev'
39
+ Provides-Extra: docs
40
+ Requires-Dist: sphinx-rtd-theme>=3.0.0; extra == 'docs'
41
+ Requires-Dist: sphinx>=8.0.0; extra == 'docs'
42
+ Provides-Extra: test
43
+ Requires-Dist: pytest-asyncio>=0.25.0; extra == 'test'
44
+ Requires-Dist: pytest-cov>=6.0.0; extra == 'test'
45
+ Requires-Dist: pytest>=8.4.1; extra == 'test'
27
46
  Description-Content-Type: text/markdown
28
47
 
29
48
  # Cista Web Storage
@@ -34,6 +53,8 @@ Cista takes its name from the ancient *cistae*, metal containers used by Greeks
34
53
 
35
54
  This is a cutting-edge **file and document server** designed for speed, efficiency, and unparalleled ease of use. Experience **lightning-fast browsing**, thanks to the file list maintained directly in your browser and updated from server filesystem events, coupled with our highly optimized code. Fully **keyboard-navigable** and with a responsive layout, Cista flawlessly adapts to your devices, providing a seamless experience wherever you are. Our powerful **instant search** means you're always just a few keystrokes away from finding exactly what you need. Press **1/2/3** to switch ordering, navigate with all four arrow keys (+Shift to select). Or click your way around on **breadcrumbs that remember where you were**.
36
55
 
56
+ **Built-in document and media previews** let you quickly view files without downloading them. Cista shows PDF and other documents, video and image thumbnails, with **HDR10 support** video previews and image formats, including HEIC and AVIF. It also has a player for music and video files.
57
+
37
58
  The Cista project started as an inevitable remake of [Droppy](https://github.com/droppyjs/droppy) which we used and loved despite its numerous bugs. Cista Storage stands out in handling even the most exotic filenames, ensuring a smooth experience where others falter.
38
59
 
39
60
  All of this is wrapped in an intuitive interface with automatic light and dark themes, making Cista Storage the ideal choice for anyone seeking a reliable, versatile, and quick file storage solution. Quickly setup your own Cista where your files are just a click away, safe, and always accessible.
@@ -42,39 +63,31 @@ Experience Cista by visiting [Cista Demo](https://drop.zi.fi) for a test run and
42
63
 
43
64
 
44
65
  ## Getting Started
45
- ### Installation
66
+ ### Running the Server
46
67
 
47
- To install the cista application, use:
68
+ We recommend using [UV](https://docs.astral.sh/uv/getting-started/installation/) to directly run Cista:
48
69
 
70
+ Create an account: (otherwise the server is public for all)
49
71
  ```fish
50
- pip install cista
72
+ uvx cista --user yourname --privileged
51
73
  ```
52
74
 
53
- Note: Some Linux distributions might need `--break-system-packages` to install Python packages, which are safely installed in the user's home folder. As an alternative to avoid installation, run it with command `pipx run cista`
54
-
55
- ### Running the Server
56
-
57
- Create an account: (or run a public server without authentication)
75
+ Serve your files at http://localhost:8000:
58
76
  ```fish
59
- cista --user yourname --privileged
77
+ uvx cista -l :8000 /path/to/files
60
78
  ```
61
79
 
62
- Serve your files at http://localhost:8000:
80
+ Alternatively, you can install with `pip` or `uv pip`. This enables using the `cista` command directly without `uvx` or `uv run`.
81
+
63
82
  ```fish
64
- cista -l :8000 /path/to/files
83
+ pip install cista --break-system-packages
65
84
  ```
66
85
 
67
86
  The server remembers its settings in the config folder (default `~/.local/share/cista/`), including the listen port and directory, for future runs without arguments.
68
87
 
69
88
  ### Internet Access
70
89
 
71
- To use your own TLS certificates, place them in the config folder and run:
72
-
73
- ```fish
74
- cista -l cista.example.com
75
- ```
76
-
77
- Most admins instead find the [Caddy](https://caddyserver.com/) web server convenient for its auto TLS certificates and all. A proxy also allows running multiple web services or Cista instances on the same IP address but different (sub)domains.
90
+ Most admins find the [Caddy](https://caddyserver.com/) web server convenient for its auto TLS certificates and all. A proxy also allows running multiple web services or Cista instances on the same IP address but different (sub)domains.
78
91
 
79
92
  `/etc/caddy/Caddyfile`:
80
93
 
@@ -84,33 +97,13 @@ cista.example.com {
84
97
  }
85
98
  ```
86
99
 
87
- ## Development setup
88
-
89
- For rapid development, we use the Vite development server for the Vue frontend, while running the backend on port 8000 that Vite proxies backend requests to. Each server live reloads whenever its code or configuration are modified.
90
-
91
- ```fish
92
- cd frontend
93
- npm install
94
- npm run dev
95
- ```
96
-
97
- Concurrently, start the backend on another terminal:
98
-
99
- ```fish
100
- hatch shell
101
- pip install -e '.[dev]'
102
- cista --dev -l :8000 /path/to/files
103
- ```
104
-
105
- We use `hatch shell` for installing on a virtual environment, to avoid disturbing the rest of the system with our hacking.
106
-
107
- Vue is used to build files in `cista/wwwroot`, included prebuilt in the Python package. Running `hatch build` builds the frontend and creates a NodeJS-independent Python package.
100
+ Nxing or other proxy may be similarly used, or alternatively you can place cert and key in cista config dir and run `cista -l cista.example.com`
108
101
 
109
102
  ## System Deployment
110
103
 
111
104
  This setup allows easy addition of storages, each with its own domain, configuration, and files.
112
105
 
113
- Assuming a restricted user account `storage` for serving files and that cista is installed system-wide or on this account (check with `sudo -u storage -s`). Alternatively, use `pipx run cista` or `hatch run cista` as the ExecStart command.
106
+ Assuming a restricted user account `storage` for serving files and that UV is installed system-wide or on this account. Only UV is required: this does not use git or bun/npm.
114
107
 
115
108
  Create `/etc/systemd/system/cista@.service`:
116
109
 
@@ -120,7 +113,7 @@ Description=Cista storage %i
120
113
 
121
114
  [Service]
122
115
  User=storage
123
- ExecStart=cista -c /srv/cista/%i -l /srv/cista/%i/socket /media/storage/%i
116
+ ExecStart=uvx cista -c /srv/cista/%i -l /srv/cista/%i/socket /media/storage/%i
124
117
  Restart=always
125
118
 
126
119
  [Install]
@@ -144,3 +137,34 @@ foo.example.com, bar.example.com {
144
137
  reverse_proxy unix//srv/cista/{host}/socket
145
138
  }
146
139
  ```
140
+
141
+ ## Development setup
142
+
143
+ For rapid development, we use the Vite development server for the Vue frontend, while running the backend on port 8000 that Vite proxies backend requests to. Each server live reloads whenever its code or configuration are modified.
144
+
145
+ Make sure you have git, uv and bun (or npm) installed.
146
+
147
+ Backend (Python) – setup and run:
148
+
149
+ ```fish
150
+ git clone https://git.zi.fi/Vasanko/cista-storage.git
151
+ cd cista-storage
152
+ uv sync --dev
153
+ uv run cista --dev -l :8000 /path/to/files
154
+ ```
155
+
156
+ Frontend (Vue/Vite) – run the dev server in another terminal:
157
+
158
+ ```fish
159
+ cd frontend
160
+ bun install
161
+ bun run dev
162
+ ```
163
+
164
+ Building the package for release (frontend + Python wheel/sdist):
165
+
166
+ ```fish
167
+ uv build
168
+ ```
169
+
170
+ Vue is used to build files in `cista/wwwroot`, included prebuilt in the Python package. `uv build` runs the project build hooks to bundle the frontend and produce a NodeJS-independent Python package.
@@ -6,6 +6,8 @@ Cista takes its name from the ancient *cistae*, metal containers used by Greeks
6
6
 
7
7
  This is a cutting-edge **file and document server** designed for speed, efficiency, and unparalleled ease of use. Experience **lightning-fast browsing**, thanks to the file list maintained directly in your browser and updated from server filesystem events, coupled with our highly optimized code. Fully **keyboard-navigable** and with a responsive layout, Cista flawlessly adapts to your devices, providing a seamless experience wherever you are. Our powerful **instant search** means you're always just a few keystrokes away from finding exactly what you need. Press **1/2/3** to switch ordering, navigate with all four arrow keys (+Shift to select). Or click your way around on **breadcrumbs that remember where you were**.
8
8
 
9
+ **Built-in document and media previews** let you quickly view files without downloading them. Cista shows PDF and other documents, video and image thumbnails, with **HDR10 support** video previews and image formats, including HEIC and AVIF. It also has a player for music and video files.
10
+
9
11
  The Cista project started as an inevitable remake of [Droppy](https://github.com/droppyjs/droppy) which we used and loved despite its numerous bugs. Cista Storage stands out in handling even the most exotic filenames, ensuring a smooth experience where others falter.
10
12
 
11
13
  All of this is wrapped in an intuitive interface with automatic light and dark themes, making Cista Storage the ideal choice for anyone seeking a reliable, versatile, and quick file storage solution. Quickly setup your own Cista where your files are just a click away, safe, and always accessible.
@@ -14,39 +16,31 @@ Experience Cista by visiting [Cista Demo](https://drop.zi.fi) for a test run and
14
16
 
15
17
 
16
18
  ## Getting Started
17
- ### Installation
19
+ ### Running the Server
18
20
 
19
- To install the cista application, use:
21
+ We recommend using [UV](https://docs.astral.sh/uv/getting-started/installation/) to directly run Cista:
20
22
 
23
+ Create an account: (otherwise the server is public for all)
21
24
  ```fish
22
- pip install cista
25
+ uvx cista --user yourname --privileged
23
26
  ```
24
27
 
25
- Note: Some Linux distributions might need `--break-system-packages` to install Python packages, which are safely installed in the user's home folder. As an alternative to avoid installation, run it with command `pipx run cista`
26
-
27
- ### Running the Server
28
-
29
- Create an account: (or run a public server without authentication)
28
+ Serve your files at http://localhost:8000:
30
29
  ```fish
31
- cista --user yourname --privileged
30
+ uvx cista -l :8000 /path/to/files
32
31
  ```
33
32
 
34
- Serve your files at http://localhost:8000:
33
+ Alternatively, you can install with `pip` or `uv pip`. This enables using the `cista` command directly without `uvx` or `uv run`.
34
+
35
35
  ```fish
36
- cista -l :8000 /path/to/files
36
+ pip install cista --break-system-packages
37
37
  ```
38
38
 
39
39
  The server remembers its settings in the config folder (default `~/.local/share/cista/`), including the listen port and directory, for future runs without arguments.
40
40
 
41
41
  ### Internet Access
42
42
 
43
- To use your own TLS certificates, place them in the config folder and run:
44
-
45
- ```fish
46
- cista -l cista.example.com
47
- ```
48
-
49
- Most admins instead find the [Caddy](https://caddyserver.com/) web server convenient for its auto TLS certificates and all. A proxy also allows running multiple web services or Cista instances on the same IP address but different (sub)domains.
43
+ Most admins find the [Caddy](https://caddyserver.com/) web server convenient for its auto TLS certificates and all. A proxy also allows running multiple web services or Cista instances on the same IP address but different (sub)domains.
50
44
 
51
45
  `/etc/caddy/Caddyfile`:
52
46
 
@@ -56,33 +50,13 @@ cista.example.com {
56
50
  }
57
51
  ```
58
52
 
59
- ## Development setup
60
-
61
- For rapid development, we use the Vite development server for the Vue frontend, while running the backend on port 8000 that Vite proxies backend requests to. Each server live reloads whenever its code or configuration are modified.
62
-
63
- ```fish
64
- cd frontend
65
- npm install
66
- npm run dev
67
- ```
68
-
69
- Concurrently, start the backend on another terminal:
70
-
71
- ```fish
72
- hatch shell
73
- pip install -e '.[dev]'
74
- cista --dev -l :8000 /path/to/files
75
- ```
76
-
77
- We use `hatch shell` for installing on a virtual environment, to avoid disturbing the rest of the system with our hacking.
78
-
79
- Vue is used to build files in `cista/wwwroot`, included prebuilt in the Python package. Running `hatch build` builds the frontend and creates a NodeJS-independent Python package.
53
+ Nxing or other proxy may be similarly used, or alternatively you can place cert and key in cista config dir and run `cista -l cista.example.com`
80
54
 
81
55
  ## System Deployment
82
56
 
83
57
  This setup allows easy addition of storages, each with its own domain, configuration, and files.
84
58
 
85
- Assuming a restricted user account `storage` for serving files and that cista is installed system-wide or on this account (check with `sudo -u storage -s`). Alternatively, use `pipx run cista` or `hatch run cista` as the ExecStart command.
59
+ Assuming a restricted user account `storage` for serving files and that UV is installed system-wide or on this account. Only UV is required: this does not use git or bun/npm.
86
60
 
87
61
  Create `/etc/systemd/system/cista@.service`:
88
62
 
@@ -92,7 +66,7 @@ Description=Cista storage %i
92
66
 
93
67
  [Service]
94
68
  User=storage
95
- ExecStart=cista -c /srv/cista/%i -l /srv/cista/%i/socket /media/storage/%i
69
+ ExecStart=uvx cista -c /srv/cista/%i -l /srv/cista/%i/socket /media/storage/%i
96
70
  Restart=always
97
71
 
98
72
  [Install]
@@ -116,3 +90,34 @@ foo.example.com, bar.example.com {
116
90
  reverse_proxy unix//srv/cista/{host}/socket
117
91
  }
118
92
  ```
93
+
94
+ ## Development setup
95
+
96
+ For rapid development, we use the Vite development server for the Vue frontend, while running the backend on port 8000 that Vite proxies backend requests to. Each server live reloads whenever its code or configuration are modified.
97
+
98
+ Make sure you have git, uv and bun (or npm) installed.
99
+
100
+ Backend (Python) – setup and run:
101
+
102
+ ```fish
103
+ git clone https://git.zi.fi/Vasanko/cista-storage.git
104
+ cd cista-storage
105
+ uv sync --dev
106
+ uv run cista --dev -l :8000 /path/to/files
107
+ ```
108
+
109
+ Frontend (Vue/Vite) – run the dev server in another terminal:
110
+
111
+ ```fish
112
+ cd frontend
113
+ bun install
114
+ bun run dev
115
+ ```
116
+
117
+ Building the package for release (frontend + Python wheel/sdist):
118
+
119
+ ```fish
120
+ uv build
121
+ ```
122
+
123
+ Vue is used to build files in `cista/wwwroot`, included prebuilt in the Python package. `uv build` runs the project build hooks to bundle the frontend and produce a NodeJS-independent Python package.
@@ -1,2 +1,2 @@
1
1
  # This file is automatically generated by hatch build.
2
- __version__ = '0.7.2'
2
+ __version__ = '1.0.0'
@@ -0,0 +1,225 @@
1
+ import asyncio
2
+ import gc
3
+ import io
4
+ import mimetypes
5
+ import urllib.parse
6
+ from pathlib import PurePosixPath
7
+ from time import perf_counter
8
+ from urllib.parse import unquote
9
+ from wsgiref.handlers import format_date_time
10
+
11
+ import av
12
+ import fitz # PyMuPDF
13
+ import numpy as np
14
+ import pillow_heif
15
+ from PIL import Image
16
+ from sanic import Blueprint, empty, raw
17
+ from sanic.exceptions import NotFound
18
+ from sanic.log import logger
19
+
20
+ from cista import config
21
+ from cista.util.filename import sanitize
22
+
23
+ pillow_heif.register_heif_opener()
24
+
25
+ bp = Blueprint("preview", url_prefix="/preview")
26
+
27
+
28
+ @bp.get("/<path:path>")
29
+ async def preview(req, path):
30
+ """Preview a file"""
31
+ maxsize = int(req.args.get("px", 1024))
32
+ maxzoom = float(req.args.get("zoom", 2.0))
33
+ quality = int(req.args.get("q", 60))
34
+ rel = PurePosixPath(sanitize(unquote(path)))
35
+ path = config.config.path / rel
36
+ stat = path.lstat()
37
+ etag = config.derived_secret(
38
+ "preview", rel, stat.st_mtime_ns, quality, maxsize, maxzoom
39
+ ).hex()
40
+ savename = PurePosixPath(path.name).with_suffix(".avif")
41
+ headers = {
42
+ "etag": etag,
43
+ "last-modified": format_date_time(stat.st_mtime),
44
+ "cache-control": "max-age=604800, immutable"
45
+ + ("" if config.config.public else ", private"),
46
+ "content-type": "image/avif",
47
+ "content-disposition": f"inline; filename*=UTF-8''{urllib.parse.quote(savename.as_posix())}",
48
+ }
49
+ if req.headers.if_none_match == etag:
50
+ # The client has it cached, respond 304 Not Modified
51
+ return empty(304, headers=headers)
52
+
53
+ if not path.is_file():
54
+ raise NotFound("File not found")
55
+
56
+ img = await asyncio.get_event_loop().run_in_executor(
57
+ req.app.ctx.threadexec, dispatch, path, quality, maxsize, maxzoom
58
+ )
59
+ return raw(img, headers=headers)
60
+
61
+
62
+ def dispatch(path, quality, maxsize, maxzoom):
63
+ if path.suffix.lower() in (".pdf", ".xps", ".epub", ".mobi"):
64
+ return process_pdf(path, quality=quality, maxsize=maxsize, maxzoom=maxzoom)
65
+ type, _ = mimetypes.guess_type(path.name)
66
+ if type and type.startswith("video/"):
67
+ return process_video(path, quality=quality, maxsize=maxsize)
68
+ return process_image(path, quality=quality, maxsize=maxsize)
69
+
70
+
71
+ def process_image(path, *, maxsize, quality):
72
+ t_load_start = perf_counter()
73
+ img = Image.open(path)
74
+ # Force decode to include I/O in load timing
75
+ img.load()
76
+ t_load_end = perf_counter()
77
+ # Resize
78
+ orig_w, orig_h = img.size
79
+ t_proc_start = perf_counter()
80
+ img.thumbnail((min(orig_w, maxsize), min(orig_h, maxsize)))
81
+ t_proc_end = perf_counter()
82
+ # Save as AVIF
83
+ imgdata = io.BytesIO()
84
+ t_save_start = perf_counter()
85
+ img.save(imgdata, format="avif", quality=quality, speed=10, max_threads=1)
86
+ t_save_end = perf_counter()
87
+
88
+ ret = imgdata.getvalue()
89
+
90
+ load_ms = (t_load_end - t_load_start) * 1000
91
+ proc_ms = (t_proc_end - t_proc_start) * 1000
92
+ save_ms = (t_save_end - t_save_start) * 1000
93
+ logger.debug(
94
+ "Preview image %s: load=%.1fms process=%.1fms save=%.1fms out=%.1fKB",
95
+ path.name,
96
+ load_ms,
97
+ proc_ms,
98
+ save_ms,
99
+ len(ret) / 1024,
100
+ )
101
+
102
+ return ret
103
+
104
+
105
+ def process_pdf(path, *, maxsize, maxzoom, quality, page_number=0):
106
+ t_load_start = perf_counter()
107
+ pdf = fitz.open(path)
108
+ page = pdf.load_page(page_number)
109
+ w, h = page.rect[2:4]
110
+ zoom = min(maxsize / w, maxsize / h, maxzoom)
111
+ mat = fitz.Matrix(zoom, zoom)
112
+ pix = page.get_pixmap(matrix=mat) # type: ignore[attr-defined]
113
+ t_load_end = perf_counter()
114
+
115
+ t_save_start = perf_counter()
116
+ ret = pix.pil_tobytes(format="avif", quality=quality, speed=10, max_threads=1)
117
+ t_save_end = perf_counter()
118
+
119
+ logger.debug(
120
+ "Preview pdf %s: load+render=%.1fms save=%.1fms",
121
+ path.name,
122
+ (t_load_end - t_load_start) * 1000,
123
+ (t_save_end - t_save_start) * 1000,
124
+ )
125
+ return ret
126
+
127
+
128
+ def process_video(path, *, maxsize, quality):
129
+ frame = None
130
+ imgdata = io.BytesIO()
131
+ istream = ostream = icc = occ = frame = None
132
+ t_load_start = perf_counter()
133
+ # Initialize to avoid "possibly unbound" in static analysis when exceptions occur
134
+ t_load_end = t_load_start
135
+ t_save_start = t_load_start
136
+ t_save_end = t_load_start
137
+ with (
138
+ av.open(str(path)) as icontainer,
139
+ av.open(imgdata, "w", format="avif") as ocontainer,
140
+ ):
141
+ istream = icontainer.streams.video[0]
142
+ istream.codec_context.skip_frame = "NONKEY"
143
+ icontainer.seek((icontainer.duration or 0) // 8)
144
+ for frame in icontainer.decode(istream):
145
+ if frame.dts is not None:
146
+ break
147
+ else:
148
+ raise RuntimeError("No frames found in video")
149
+
150
+ # Resize frame to thumbnail size
151
+ if frame.width > maxsize or frame.height > maxsize:
152
+ scale_factor = min(maxsize / frame.width, maxsize / frame.height)
153
+ new_width = int(frame.width * scale_factor)
154
+ new_height = int(frame.height * scale_factor)
155
+ frame = frame.reformat(width=new_width, height=new_height)
156
+
157
+ # Simple rotation detection and logging
158
+ if frame.rotation:
159
+ try:
160
+ fplanes = frame.to_ndarray()
161
+ # Split into Y, U, V planes of proper dimensions
162
+ planes = [
163
+ fplanes[: frame.height],
164
+ fplanes[frame.height : frame.height + frame.height // 4].reshape(
165
+ frame.height // 2, frame.width // 2
166
+ ),
167
+ fplanes[frame.height + frame.height // 4 :].reshape(
168
+ frame.height // 2, frame.width // 2
169
+ ),
170
+ ]
171
+ # Rotate
172
+ planes = [np.rot90(p, frame.rotation // 90) for p in planes]
173
+ # Restore PyAV format
174
+ planes = np.hstack([p.flat for p in planes]).reshape(
175
+ -1, planes[0].shape[1]
176
+ )
177
+ frame = av.VideoFrame.from_ndarray(planes, format=frame.format.name)
178
+ del planes, fplanes
179
+ except Exception as e:
180
+ if "not yet supported" in str(e):
181
+ logger.warning(
182
+ f"Not rotating {path.name} preview image by {frame.rotation}°:\n PyAV: {e}"
183
+ )
184
+ else:
185
+ logger.exception(f"Error rotating video frame: {e}")
186
+ t_load_end = perf_counter()
187
+
188
+ t_save_start = perf_counter()
189
+ crf = str(int(63 * (1 - quality / 100) ** 2)) # Closely matching PIL quality-%
190
+ ostream = ocontainer.add_stream(
191
+ "av1",
192
+ options={
193
+ "crf": crf,
194
+ "usage": "realtime",
195
+ "cpu-used": "8",
196
+ "threads": "1",
197
+ },
198
+ )
199
+ assert isinstance(ostream, av.VideoStream)
200
+ ostream.width = frame.width
201
+ ostream.height = frame.height
202
+ icc = istream.codec_context
203
+ occ = ostream.codec_context
204
+
205
+ # Copy HDR metadata from input video stream
206
+ occ.color_primaries = icc.color_primaries
207
+ occ.color_trc = icc.color_trc
208
+ occ.colorspace = icc.colorspace
209
+ occ.color_range = icc.color_range
210
+
211
+ ocontainer.mux(ostream.encode(frame))
212
+ ocontainer.mux(ostream.encode(None)) # Flush the stream
213
+ t_save_end = perf_counter()
214
+
215
+ # Capture frame dimensions before cleanup
216
+ ret = imgdata.getvalue()
217
+ logger.debug(
218
+ "Preview video %s: load+decode=%.1fms save=%.1fms",
219
+ path.name,
220
+ (t_load_end - t_load_start) * 1000,
221
+ (t_save_end - t_save_start) * 1000,
222
+ )
223
+ del imgdata, istream, ostream, icc, occ, frame
224
+ gc.collect()
225
+ return ret
@@ -127,8 +127,7 @@ class FileEntry(msgspec.Struct, array_like=True, frozen=True):
127
127
  return f"{self.name} ({self.size}, {self.mtime})"
128
128
 
129
129
 
130
- class Update(msgspec.Struct, array_like=True):
131
- ...
130
+ class Update(msgspec.Struct, array_like=True): ...
132
131
 
133
132
 
134
133
  class UpdKeep(Update, tag="k"):
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  import re
3
+ import signal
3
4
  from pathlib import Path
4
5
 
5
6
  from sanic import Sanic
@@ -11,6 +12,14 @@ def run(*, dev=False):
11
12
  """Run Sanic main process that spawns worker processes to serve HTTP requests."""
12
13
  from .app import app
13
14
 
15
+ # Set up immediate exit on Ctrl+C for faster termination
16
+ def signal_handler(signum, frame):
17
+ print("\nReceived interrupt signal, exiting immediately...")
18
+ os._exit(0)
19
+
20
+ signal.signal(signal.SIGINT, signal_handler)
21
+ signal.signal(signal.SIGTERM, signal_handler)
22
+
14
23
  url, opts = parse_listen(config.config.listen)
15
24
  # Silence Sanic's warning about running in production rather than debug
16
25
  os.environ["SANIC_IGNORE_PRODUCTION_WARNING"] = "1"
@@ -29,6 +29,8 @@ async def handle_sanic_exception(request, e):
29
29
  if not message or not request.app.debug and code == 500:
30
30
  message = "Internal Server Error"
31
31
  message = f"⚠️ {message}" if code < 500 else f"🛑 {message}"
32
+ if code == 500:
33
+ logger.exception(e)
32
34
  # Non-browsers get JSON errors
33
35
  if "text/html" not in request.headers.accept:
34
36
  return jres(