cista 0.7.2__tar.gz → 1.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 (146) hide show
  1. {cista-0.7.2 → cista-1.1.0}/.gitignore +1 -0
  2. {cista-0.7.2 → cista-1.1.0}/PKG-INFO +85 -61
  3. {cista-0.7.2 → cista-1.1.0}/README.md +46 -41
  4. {cista-0.7.2 → cista-1.1.0}/cista/_version.py +1 -1
  5. {cista-0.7.2 → cista-1.1.0}/cista/api.py +5 -1
  6. {cista-0.7.2 → cista-1.1.0}/cista/app.py +5 -3
  7. cista-1.1.0/cista/preview.py +237 -0
  8. {cista-0.7.2 → cista-1.1.0}/cista/protocol.py +1 -2
  9. {cista-0.7.2 → cista-1.1.0}/cista/util/apphelpers.py +2 -0
  10. {cista-0.7.2 → cista-1.1.0}/cista/watching.py +101 -38
  11. cista-1.1.0/cista/wwwroot/assets/add-file-6a41e251.js +1 -0
  12. cista-1.1.0/cista/wwwroot/assets/add-folder-62d934ef.js +1 -0
  13. cista-1.1.0/cista/wwwroot/assets/arrow-a3a583f8.js +1 -0
  14. cista-1.1.0/cista/wwwroot/assets/arrows-h-d9e36082.js +1 -0
  15. cista-1.1.0/cista/wwwroot/assets/arrows-v-5223491a.js +1 -0
  16. cista-1.1.0/cista/wwwroot/assets/check-f7566407.js +1 -0
  17. cista-1.1.0/cista/wwwroot/assets/code-8e0123d4.js +1 -0
  18. cista-1.1.0/cista/wwwroot/assets/copy-a70a3d1c.js +1 -0
  19. cista-1.1.0/cista/wwwroot/assets/create-file-d995875e.js +1 -0
  20. cista-1.1.0/cista/wwwroot/assets/create-folder-c448deda.js +1 -0
  21. cista-1.1.0/cista/wwwroot/assets/cross-e74ed6f4.js +1 -0
  22. cista-1.1.0/cista/wwwroot/assets/disk-f1376321.js +1 -0
  23. cista-1.1.0/cista/wwwroot/assets/download-bad67930.js +1 -0
  24. cista-1.1.0/cista/wwwroot/assets/exclamation-afec06bb.js +1 -0
  25. cista-1.1.0/cista/wwwroot/assets/eye-5fde34a1.js +1 -0
  26. cista-1.1.0/cista/wwwroot/assets/find-11711648.js +1 -0
  27. cista-1.1.0/cista/wwwroot/assets/fullscreen-7f61e522.js +1 -0
  28. cista-1.1.0/cista/wwwroot/assets/github-c9475017.js +1 -0
  29. cista-1.1.0/cista/wwwroot/assets/index-0ccc4ea2.js +28 -0
  30. cista-1.1.0/cista/wwwroot/assets/index-89d850f4.css +1 -0
  31. cista-1.1.0/cista/wwwroot/assets/info-b3d76e57.js +1 -0
  32. cista-1.1.0/cista/wwwroot/assets/link-5cd893e4.js +1 -0
  33. cista-1.1.0/cista/wwwroot/assets/logo-6090454d.js +1 -0
  34. cista-1.1.0/cista/wwwroot/assets/loop-531f6994.js +1 -0
  35. cista-1.1.0/cista/wwwroot/assets/menu-ed0d8c47.js +1 -0
  36. cista-1.1.0/cista/wwwroot/assets/next-ee82241a.js +1 -0
  37. cista-1.1.0/cista/wwwroot/assets/open-8364df82.js +1 -0
  38. cista-1.1.0/cista/wwwroot/assets/paste-0f86e193.js +1 -0
  39. cista-1.1.0/cista/wwwroot/assets/pause-a2dd4670.js +1 -0
  40. cista-1.1.0/cista/wwwroot/assets/pencil-bfd02151.js +1 -0
  41. cista-1.1.0/cista/wwwroot/assets/play-83e40a03.js +1 -0
  42. cista-1.1.0/cista/wwwroot/assets/plus-f463cfbc.js +1 -0
  43. cista-1.1.0/cista/wwwroot/assets/previous-cbb778b5.js +1 -0
  44. cista-1.1.0/cista/wwwroot/assets/reload-907eb866.js +1 -0
  45. cista-1.1.0/cista/wwwroot/assets/rename-51dadde6.js +1 -0
  46. cista-1.1.0/cista/wwwroot/assets/scissors-d32350cc.js +1 -0
  47. cista-1.1.0/cista/wwwroot/assets/shuffle-00a003ad.js +1 -0
  48. cista-1.1.0/cista/wwwroot/assets/signin-d7bd57fd.js +1 -0
  49. cista-1.1.0/cista/wwwroot/assets/signout-4c4ff7fb.js +1 -0
  50. cista-1.1.0/cista/wwwroot/assets/skip-0069d8d7.js +1 -0
  51. cista-1.1.0/cista/wwwroot/assets/spinner-164c9b34.js +1 -0
  52. cista-1.1.0/cista/wwwroot/assets/stop-3717c27d.js +1 -0
  53. cista-1.1.0/cista/wwwroot/assets/trash-449f81ef.js +1 -0
  54. cista-1.1.0/cista/wwwroot/assets/triangle-3ecaf29c.js +1 -0
  55. cista-1.1.0/cista/wwwroot/assets/unfullscreen-175d46cd.js +1 -0
  56. cista-1.1.0/cista/wwwroot/assets/up-arrow-164a366e.js +1 -0
  57. cista-1.1.0/cista/wwwroot/assets/upload-cloud-df377f1a.js +1 -0
  58. cista-1.1.0/cista/wwwroot/assets/user-3b5a32bc.js +1 -0
  59. cista-1.1.0/cista/wwwroot/assets/user-cog-27e2c201.js +1 -0
  60. cista-1.1.0/cista/wwwroot/assets/volume-high-f4e07edd.js +1 -0
  61. cista-1.1.0/cista/wwwroot/assets/volume-low-f754322d.js +1 -0
  62. cista-1.1.0/cista/wwwroot/assets/volume-medium-a5806088.js +1 -0
  63. cista-1.1.0/cista/wwwroot/assets/volume-mute-405763b1.js +1 -0
  64. cista-1.1.0/cista/wwwroot/assets/window-cross-86cb8236.js +1 -0
  65. cista-1.1.0/cista/wwwroot/assets/window-d99968ac.js +1 -0
  66. cista-1.1.0/cista/wwwroot/assets/wordwrap-cdcd3f93.js +1 -0
  67. cista-1.1.0/cista/wwwroot/assets/zoomin-7d13bb9b.js +1 -0
  68. cista-1.1.0/cista/wwwroot/assets/zoomout-daf52018.js +1 -0
  69. {cista-0.7.2 → cista-1.1.0}/cista/wwwroot/index.html +2 -2
  70. cista-1.1.0/pyproject.toml +154 -0
  71. cista-0.7.2/cista/preview.py +0 -117
  72. cista-0.7.2/cista/wwwroot/assets/add-file-0ff53f2c.js +0 -1
  73. cista-0.7.2/cista/wwwroot/assets/add-folder-0ac641fd.js +0 -1
  74. cista-0.7.2/cista/wwwroot/assets/arrow-6d57743c.js +0 -1
  75. cista-0.7.2/cista/wwwroot/assets/arrows-h-9715a1dc.js +0 -1
  76. cista-0.7.2/cista/wwwroot/assets/arrows-v-17478ebc.js +0 -1
  77. cista-0.7.2/cista/wwwroot/assets/check-b37d535a.js +0 -1
  78. cista-0.7.2/cista/wwwroot/assets/code-35dab0d8.js +0 -1
  79. cista-0.7.2/cista/wwwroot/assets/copy-08cc120b.js +0 -1
  80. cista-0.7.2/cista/wwwroot/assets/create-file-48e89b04.js +0 -1
  81. cista-0.7.2/cista/wwwroot/assets/create-folder-b9d2fd99.js +0 -1
  82. cista-0.7.2/cista/wwwroot/assets/cross-9008d2a4.js +0 -1
  83. cista-0.7.2/cista/wwwroot/assets/disk-5e08f410.js +0 -1
  84. cista-0.7.2/cista/wwwroot/assets/download-72de2989.js +0 -1
  85. cista-0.7.2/cista/wwwroot/assets/exclamation-a7d9f408.js +0 -1
  86. cista-0.7.2/cista/wwwroot/assets/eye-73e4e6be.js +0 -1
  87. cista-0.7.2/cista/wwwroot/assets/find-ec0a7765.js +0 -1
  88. cista-0.7.2/cista/wwwroot/assets/fullscreen-7cf77f36.js +0 -1
  89. cista-0.7.2/cista/wwwroot/assets/github-768c474d.js +0 -1
  90. cista-0.7.2/cista/wwwroot/assets/index-1332f321.js +0 -13
  91. cista-0.7.2/cista/wwwroot/assets/index-456bb401.css +0 -1
  92. cista-0.7.2/cista/wwwroot/assets/info-c28d09ff.js +0 -1
  93. cista-0.7.2/cista/wwwroot/assets/link-8cf3bdf0.js +0 -1
  94. cista-0.7.2/cista/wwwroot/assets/logo-d5f794ec.js +0 -1
  95. cista-0.7.2/cista/wwwroot/assets/loop-fedc4169.js +0 -1
  96. cista-0.7.2/cista/wwwroot/assets/menu-0c34e3e2.js +0 -1
  97. cista-0.7.2/cista/wwwroot/assets/next-0a58f144.js +0 -1
  98. cista-0.7.2/cista/wwwroot/assets/open-35afd848.js +0 -1
  99. cista-0.7.2/cista/wwwroot/assets/paste-e0ef001a.js +0 -1
  100. cista-0.7.2/cista/wwwroot/assets/pause-a35acf21.js +0 -1
  101. cista-0.7.2/cista/wwwroot/assets/pencil-cd90f93a.js +0 -1
  102. cista-0.7.2/cista/wwwroot/assets/play-f535c62d.js +0 -1
  103. cista-0.7.2/cista/wwwroot/assets/plus-b2df8058.js +0 -1
  104. cista-0.7.2/cista/wwwroot/assets/previous-85fd5e4d.js +0 -1
  105. cista-0.7.2/cista/wwwroot/assets/reload-e27d8eda.js +0 -1
  106. cista-0.7.2/cista/wwwroot/assets/rename-a6cb8477.js +0 -1
  107. cista-0.7.2/cista/wwwroot/assets/scissors-c98a2f01.js +0 -1
  108. cista-0.7.2/cista/wwwroot/assets/shuffle-07c5c925.js +0 -1
  109. cista-0.7.2/cista/wwwroot/assets/signin-c2a74424.js +0 -1
  110. cista-0.7.2/cista/wwwroot/assets/signout-ce13f56f.js +0 -1
  111. cista-0.7.2/cista/wwwroot/assets/skip-81664c53.js +0 -1
  112. cista-0.7.2/cista/wwwroot/assets/spinner-95826db5.js +0 -1
  113. cista-0.7.2/cista/wwwroot/assets/stop-acb41526.js +0 -1
  114. cista-0.7.2/cista/wwwroot/assets/trash-c8e2fdaa.js +0 -1
  115. cista-0.7.2/cista/wwwroot/assets/triangle-c25a1114.js +0 -1
  116. cista-0.7.2/cista/wwwroot/assets/unfullscreen-e7cc25a1.js +0 -1
  117. cista-0.7.2/cista/wwwroot/assets/up-arrow-a110006a.js +0 -1
  118. cista-0.7.2/cista/wwwroot/assets/upload-cloud-6cdf142b.js +0 -1
  119. cista-0.7.2/cista/wwwroot/assets/user-2d648d32.js +0 -1
  120. cista-0.7.2/cista/wwwroot/assets/user-cog-a90f18ca.js +0 -1
  121. cista-0.7.2/cista/wwwroot/assets/volume-high-792bbac7.js +0 -1
  122. cista-0.7.2/cista/wwwroot/assets/volume-low-d40f717a.js +0 -1
  123. cista-0.7.2/cista/wwwroot/assets/volume-medium-073ff083.js +0 -1
  124. cista-0.7.2/cista/wwwroot/assets/volume-mute-ed5cdbbb.js +0 -1
  125. cista-0.7.2/cista/wwwroot/assets/window-4bf01d12.js +0 -1
  126. cista-0.7.2/cista/wwwroot/assets/window-cross-d62113fd.js +0 -1
  127. cista-0.7.2/cista/wwwroot/assets/wordwrap-1032ac82.js +0 -1
  128. cista-0.7.2/cista/wwwroot/assets/zoomin-98208790.js +0 -1
  129. cista-0.7.2/cista/wwwroot/assets/zoomout-ea6810cf.js +0 -1
  130. cista-0.7.2/pyproject.toml +0 -105
  131. {cista-0.7.2 → cista-1.1.0}/cista/__init__.py +0 -0
  132. {cista-0.7.2 → cista-1.1.0}/cista/__main__.py +0 -0
  133. {cista-0.7.2 → cista-1.1.0}/cista/auth.py +0 -0
  134. {cista-0.7.2 → cista-1.1.0}/cista/config.py +0 -0
  135. {cista-0.7.2 → cista-1.1.0}/cista/droppy.py +0 -0
  136. {cista-0.7.2 → cista-1.1.0}/cista/fileio.py +0 -0
  137. {cista-0.7.2 → cista-1.1.0}/cista/serve.py +0 -0
  138. {cista-0.7.2 → cista-1.1.0}/cista/server80.py +0 -0
  139. {cista-0.7.2 → cista-1.1.0}/cista/session.py +0 -0
  140. {cista-0.7.2 → cista-1.1.0}/cista/util/__init__.py +0 -0
  141. {cista-0.7.2 → cista-1.1.0}/cista/util/asynclink.py +0 -0
  142. {cista-0.7.2 → cista-1.1.0}/cista/util/filename.py +0 -0
  143. {cista-0.7.2 → cista-1.1.0}/cista/util/lrucache.py +0 -0
  144. {cista-0.7.2 → cista-1.1.0}/cista/util/pwgen.py +0 -0
  145. {cista-0.7.2 → cista-1.1.0}/cista/wwwroot/assets/logo-97d1d7eb.svg +0 -0
  146. {cista-0.7.2 → cista-1.1.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.1.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.1.0'
@@ -119,8 +119,12 @@ async def watch(req, ws):
119
119
  # Send updates
120
120
  while True:
121
121
  await ws.send(await q.get())
122
+ except RuntimeError as e:
123
+ if str(e) == "cannot schedule new futures after shutdown":
124
+ return # Server shutting down, drop the WebSocket
125
+ raise
122
126
  finally:
123
- del watching.pubsub[uuid]
127
+ watching.pubsub.pop(uuid, None) # Remove whether it got added yet or not
124
128
 
125
129
 
126
130
  def subscribe(uuid, ws):
@@ -43,14 +43,16 @@ async def main_start(app, loop):
43
43
  app.ctx.threadexec = ThreadPoolExecutor(
44
44
  max_workers=workers, thread_name_prefix="cista-ioworker"
45
45
  )
46
- await watching.start(app, loop)
46
+ watching.start(app, loop)
47
47
 
48
48
 
49
- @app.after_server_stop
49
+ # Sanic sometimes fails to execute after_server_stop, so we do it before instead (potentially interrupting handlers)
50
+ @app.before_server_stop
50
51
  async def main_stop(app, loop):
51
52
  quit.set()
52
- await watching.stop(app, loop)
53
+ watching.stop(app)
53
54
  app.ctx.threadexec.shutdown()
55
+ logger.debug("Cista worker threads all finished")
54
56
 
55
57
 
56
58
  @app.on_request
@@ -0,0 +1,237 @@
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
+ # Map EXIF Orientation value to a corresponding PIL transpose
28
+ EXIF_ORI = {
29
+ 2: Image.Transpose.FLIP_LEFT_RIGHT,
30
+ 3: Image.Transpose.ROTATE_180,
31
+ 4: Image.Transpose.FLIP_TOP_BOTTOM,
32
+ 5: Image.Transpose.TRANSPOSE,
33
+ 6: Image.Transpose.ROTATE_270,
34
+ 7: Image.Transpose.TRANSVERSE,
35
+ 8: Image.Transpose.ROTATE_90,
36
+ }
37
+
38
+
39
+ @bp.get("/<path:path>")
40
+ async def preview(req, path):
41
+ """Preview a file"""
42
+ maxsize = int(req.args.get("px", 1024))
43
+ maxzoom = float(req.args.get("zoom", 2.0))
44
+ quality = int(req.args.get("q", 60))
45
+ rel = PurePosixPath(sanitize(unquote(path)))
46
+ path = config.config.path / rel
47
+ stat = path.lstat()
48
+ etag = config.derived_secret(
49
+ "preview", rel, stat.st_mtime_ns, quality, maxsize, maxzoom
50
+ ).hex()
51
+ savename = PurePosixPath(path.name).with_suffix(".avif")
52
+ headers = {
53
+ "etag": etag,
54
+ "last-modified": format_date_time(stat.st_mtime),
55
+ "cache-control": "max-age=604800, immutable"
56
+ + ("" if config.config.public else ", private"),
57
+ "content-type": "image/avif",
58
+ "content-disposition": f"inline; filename*=UTF-8''{urllib.parse.quote(savename.as_posix())}",
59
+ }
60
+ if req.headers.if_none_match == etag:
61
+ # The client has it cached, respond 304 Not Modified
62
+ return empty(304, headers=headers)
63
+
64
+ if not path.is_file():
65
+ raise NotFound("File not found")
66
+
67
+ img = await asyncio.get_event_loop().run_in_executor(
68
+ req.app.ctx.threadexec, dispatch, path, quality, maxsize, maxzoom
69
+ )
70
+ return raw(img, headers=headers)
71
+
72
+
73
+ def dispatch(path, quality, maxsize, maxzoom):
74
+ if path.suffix.lower() in (".pdf", ".xps", ".epub", ".mobi"):
75
+ return process_pdf(path, quality=quality, maxsize=maxsize, maxzoom=maxzoom)
76
+ type, _ = mimetypes.guess_type(path.name)
77
+ if type and type.startswith("video/"):
78
+ return process_video(path, quality=quality, maxsize=maxsize)
79
+ return process_image(path, quality=quality, maxsize=maxsize)
80
+
81
+
82
+ def process_image(path, *, maxsize, quality):
83
+ t_load = perf_counter()
84
+ with Image.open(path) as img:
85
+ # Force decode to include I/O in load timing
86
+ img.load()
87
+ t_proc = perf_counter()
88
+ # Resize
89
+ w, h = img.size
90
+ img.thumbnail((min(w, maxsize), min(h, maxsize)))
91
+ # Transpose pixels according to EXIF Orientation
92
+ orientation = img.getexif().get(274, 1)
93
+ if orientation in EXIF_ORI:
94
+ img = img.transpose(EXIF_ORI[orientation])
95
+ # Save as AVIF
96
+ imgdata = io.BytesIO()
97
+ t_save = perf_counter()
98
+ img.save(imgdata, format="avif", quality=quality, speed=10, max_threads=1)
99
+
100
+ t_end = perf_counter()
101
+ ret = imgdata.getvalue()
102
+
103
+ load_ms = (t_proc - t_load) * 1000
104
+ proc_ms = (t_save - t_proc) * 1000
105
+ save_ms = (t_end - t_save) * 1000
106
+ logger.debug(
107
+ "Preview image %s: load=%.1fms process=%.1fms save=%.1fms",
108
+ path.name,
109
+ load_ms,
110
+ proc_ms,
111
+ save_ms,
112
+ )
113
+
114
+ return ret
115
+
116
+
117
+ def process_pdf(path, *, maxsize, maxzoom, quality, page_number=0):
118
+ t_load_start = perf_counter()
119
+ pdf = fitz.open(path)
120
+ page = pdf.load_page(page_number)
121
+ w, h = page.rect[2:4]
122
+ zoom = min(maxsize / w, maxsize / h, maxzoom)
123
+ mat = fitz.Matrix(zoom, zoom)
124
+ pix = page.get_pixmap(matrix=mat) # type: ignore[attr-defined]
125
+ t_load_end = perf_counter()
126
+
127
+ t_save_start = perf_counter()
128
+ ret = pix.pil_tobytes(format="avif", quality=quality, speed=10, max_threads=1)
129
+ t_save_end = perf_counter()
130
+
131
+ logger.debug(
132
+ "Preview pdf %s: load+render=%.1fms save=%.1fms",
133
+ path.name,
134
+ (t_load_end - t_load_start) * 1000,
135
+ (t_save_end - t_save_start) * 1000,
136
+ )
137
+ return ret
138
+
139
+
140
+ def process_video(path, *, maxsize, quality):
141
+ frame = None
142
+ imgdata = io.BytesIO()
143
+ istream = ostream = icc = occ = frame = None
144
+ t_load_start = perf_counter()
145
+ # Initialize to avoid "possibly unbound" in static analysis when exceptions occur
146
+ t_load_end = t_load_start
147
+ t_save_start = t_load_start
148
+ t_save_end = t_load_start
149
+ with (
150
+ av.open(str(path)) as icontainer,
151
+ av.open(imgdata, "w", format="avif") as ocontainer,
152
+ ):
153
+ istream = icontainer.streams.video[0]
154
+ istream.codec_context.skip_frame = "NONKEY"
155
+ icontainer.seek((icontainer.duration or 0) // 8)
156
+ for frame in icontainer.decode(istream):
157
+ if frame.dts is not None:
158
+ break
159
+ else:
160
+ raise RuntimeError("No frames found in video")
161
+
162
+ # Resize frame to thumbnail size
163
+ if frame.width > maxsize or frame.height > maxsize:
164
+ scale_factor = min(maxsize / frame.width, maxsize / frame.height)
165
+ new_width = int(frame.width * scale_factor)
166
+ new_height = int(frame.height * scale_factor)
167
+ frame = frame.reformat(width=new_width, height=new_height)
168
+
169
+ # Simple rotation detection and logging
170
+ if frame.rotation:
171
+ try:
172
+ fplanes = frame.to_ndarray()
173
+ # Split into Y, U, V planes of proper dimensions
174
+ planes = [
175
+ fplanes[: frame.height],
176
+ fplanes[frame.height : frame.height + frame.height // 4].reshape(
177
+ frame.height // 2, frame.width // 2
178
+ ),
179
+ fplanes[frame.height + frame.height // 4 :].reshape(
180
+ frame.height // 2, frame.width // 2
181
+ ),
182
+ ]
183
+ # Rotate
184
+ planes = [np.rot90(p, frame.rotation // 90) for p in planes]
185
+ # Restore PyAV format
186
+ planes = np.hstack([p.flat for p in planes]).reshape(
187
+ -1, planes[0].shape[1]
188
+ )
189
+ frame = av.VideoFrame.from_ndarray(planes, format=frame.format.name)
190
+ del planes, fplanes
191
+ except Exception as e:
192
+ if "not yet supported" in str(e):
193
+ logger.warning(
194
+ f"Not rotating {path.name} preview image by {frame.rotation}°:\n PyAV: {e}"
195
+ )
196
+ else:
197
+ logger.exception(f"Error rotating video frame: {e}")
198
+ t_load_end = perf_counter()
199
+
200
+ t_save_start = perf_counter()
201
+ crf = str(int(63 * (1 - quality / 100) ** 2)) # Closely matching PIL quality-%
202
+ ostream = ocontainer.add_stream(
203
+ "av1",
204
+ options={
205
+ "crf": crf,
206
+ "usage": "realtime",
207
+ "cpu-used": "8",
208
+ "threads": "1",
209
+ },
210
+ )
211
+ assert isinstance(ostream, av.VideoStream)
212
+ ostream.width = frame.width
213
+ ostream.height = frame.height
214
+ icc = istream.codec_context
215
+ occ = ostream.codec_context
216
+
217
+ # Copy HDR metadata from input video stream
218
+ occ.color_primaries = icc.color_primaries
219
+ occ.color_trc = icc.color_trc
220
+ occ.colorspace = icc.colorspace
221
+ occ.color_range = icc.color_range
222
+
223
+ ocontainer.mux(ostream.encode(frame))
224
+ ocontainer.mux(ostream.encode(None)) # Flush the stream
225
+ t_save_end = perf_counter()
226
+
227
+ # Capture frame dimensions before cleanup
228
+ ret = imgdata.getvalue()
229
+ logger.debug(
230
+ "Preview video %s: load+decode=%.1fms save=%.1fms",
231
+ path.name,
232
+ (t_load_end - t_load_start) * 1000,
233
+ (t_save_end - t_save_start) * 1000,
234
+ )
235
+ del imgdata, istream, ostream, icc, occ, frame
236
+ gc.collect()
237
+ 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"):
@@ -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(