cista 0.5.0__tar.gz → 0.7.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 (90) hide show
  1. {cista-0.5.0 → cista-0.7.0}/PKG-INFO +15 -10
  2. {cista-0.5.0 → cista-0.7.0}/README.md +118 -116
  3. {cista-0.5.0 → cista-0.7.0}/cista/_version.py +1 -1
  4. {cista-0.5.0 → cista-0.7.0}/cista/api.py +16 -5
  5. {cista-0.5.0 → cista-0.7.0}/cista/app.py +40 -27
  6. {cista-0.5.0 → cista-0.7.0}/cista/auth.py +33 -1
  7. cista-0.7.0/cista/preview.py +114 -0
  8. {cista-0.5.0 → cista-0.7.0}/cista/protocol.py +5 -2
  9. {cista-0.5.0 → cista-0.7.0}/cista/serve.py +1 -1
  10. {cista-0.5.0 → cista-0.7.0}/cista/util/apphelpers.py +3 -3
  11. cista-0.7.0/cista/watching.py +396 -0
  12. cista-0.5.0/cista/wwwroot/assets/add-file-78f95102.js → cista-0.7.0/cista/wwwroot/assets/add-file-e19e18ff.js +1 -1
  13. cista-0.5.0/cista/wwwroot/assets/add-folder-ef91bb0e.js → cista-0.7.0/cista/wwwroot/assets/add-folder-cf930a27.js +1 -1
  14. cista-0.5.0/cista/wwwroot/assets/arrow-c279db10.js → cista-0.7.0/cista/wwwroot/assets/arrow-f09bf4bc.js +1 -1
  15. cista-0.5.0/cista/wwwroot/assets/arrows-h-0090937e.js → cista-0.7.0/cista/wwwroot/assets/arrows-h-74714fc1.js +1 -1
  16. cista-0.5.0/cista/wwwroot/assets/arrows-v-05d0dc3e.js → cista-0.7.0/cista/wwwroot/assets/arrows-v-900e3e20.js +1 -1
  17. cista-0.5.0/cista/wwwroot/assets/check-736f9156.js → cista-0.7.0/cista/wwwroot/assets/check-5d5030f9.js +1 -1
  18. cista-0.5.0/cista/wwwroot/assets/code-f442e1ae.js → cista-0.7.0/cista/wwwroot/assets/code-08d0327a.js +1 -1
  19. cista-0.5.0/cista/wwwroot/assets/copy-f9304d76.js → cista-0.7.0/cista/wwwroot/assets/copy-d4e131a3.js +1 -1
  20. cista-0.5.0/cista/wwwroot/assets/create-file-5cfbca5b.js → cista-0.7.0/cista/wwwroot/assets/create-file-7ee36655.js +1 -1
  21. cista-0.5.0/cista/wwwroot/assets/create-folder-f3416e7f.js → cista-0.7.0/cista/wwwroot/assets/create-folder-4baad31c.js +1 -1
  22. cista-0.5.0/cista/wwwroot/assets/cross-34310f33.js → cista-0.7.0/cista/wwwroot/assets/cross-5dc70d0a.js +1 -1
  23. cista-0.5.0/cista/wwwroot/assets/disk-9de14290.js → cista-0.7.0/cista/wwwroot/assets/disk-1c8c6bb2.js +1 -1
  24. cista-0.5.0/cista/wwwroot/assets/download-40f6e8d9.js → cista-0.7.0/cista/wwwroot/assets/download-4c58df40.js +1 -1
  25. cista-0.5.0/cista/wwwroot/assets/exclamation-67d2f487.js → cista-0.7.0/cista/wwwroot/assets/exclamation-b23fc5a1.js +1 -1
  26. cista-0.5.0/cista/wwwroot/assets/eye-1a9cae12.js → cista-0.7.0/cista/wwwroot/assets/eye-fc97097a.js +1 -1
  27. cista-0.5.0/cista/wwwroot/assets/find-f49bedc3.js → cista-0.7.0/cista/wwwroot/assets/find-9f5d36c7.js +1 -1
  28. cista-0.5.0/cista/wwwroot/assets/fullscreen-6e83f85d.js → cista-0.7.0/cista/wwwroot/assets/fullscreen-72bac89d.js +1 -1
  29. cista-0.5.0/cista/wwwroot/assets/github-33da7a52.js → cista-0.7.0/cista/wwwroot/assets/github-8781f34a.js +1 -1
  30. cista-0.7.0/cista/wwwroot/assets/index-5ab4ce9a.css +1 -0
  31. cista-0.7.0/cista/wwwroot/assets/index-c828fba8.js +13 -0
  32. cista-0.5.0/cista/wwwroot/assets/info-9a003b6c.js → cista-0.7.0/cista/wwwroot/assets/info-81c1e2fa.js +1 -1
  33. cista-0.5.0/cista/wwwroot/assets/link-b5bf40a7.js → cista-0.7.0/cista/wwwroot/assets/link-ddc2f9ba.js +1 -1
  34. cista-0.5.0/cista/wwwroot/assets/logo-adde2ca4.js → cista-0.7.0/cista/wwwroot/assets/logo-10d7b218.js +1 -1
  35. cista-0.5.0/cista/wwwroot/assets/loop-762ab430.js → cista-0.7.0/cista/wwwroot/assets/loop-a579040b.js +1 -1
  36. cista-0.5.0/cista/wwwroot/assets/menu-633b2a61.js → cista-0.7.0/cista/wwwroot/assets/menu-cb4bdef2.js +1 -1
  37. cista-0.5.0/cista/wwwroot/assets/next-d9d1d510.js → cista-0.7.0/cista/wwwroot/assets/next-bb1c5152.js +1 -1
  38. cista-0.5.0/cista/wwwroot/assets/open-91351e45.js → cista-0.7.0/cista/wwwroot/assets/open-f8e4da33.js +1 -1
  39. cista-0.5.0/cista/wwwroot/assets/paste-d3f337c1.js → cista-0.7.0/cista/wwwroot/assets/paste-0bef6dfd.js +1 -1
  40. cista-0.5.0/cista/wwwroot/assets/pause-8f82e536.js → cista-0.7.0/cista/wwwroot/assets/pause-27898a74.js +1 -1
  41. cista-0.5.0/cista/wwwroot/assets/pencil-0b434534.js → cista-0.7.0/cista/wwwroot/assets/pencil-19d33c49.js +1 -1
  42. cista-0.5.0/cista/wwwroot/assets/play-e0e51167.js → cista-0.7.0/cista/wwwroot/assets/play-fe6706ce.js +1 -1
  43. cista-0.5.0/cista/wwwroot/assets/plus-e2a2ec0f.js → cista-0.7.0/cista/wwwroot/assets/plus-ab9d4dbd.js +1 -1
  44. cista-0.5.0/cista/wwwroot/assets/previous-cd61ebe6.js → cista-0.7.0/cista/wwwroot/assets/previous-5be4e762.js +1 -1
  45. cista-0.5.0/cista/wwwroot/assets/reload-a9c668b2.js → cista-0.7.0/cista/wwwroot/assets/reload-84455f3f.js +1 -1
  46. cista-0.5.0/cista/wwwroot/assets/rename-bd15329b.js → cista-0.7.0/cista/wwwroot/assets/rename-88ab1b4d.js +1 -1
  47. cista-0.5.0/cista/wwwroot/assets/scissors-dcbf78c0.js → cista-0.7.0/cista/wwwroot/assets/scissors-1266cf56.js +1 -1
  48. cista-0.5.0/cista/wwwroot/assets/shuffle-74e9ea1c.js → cista-0.7.0/cista/wwwroot/assets/shuffle-0412a143.js +1 -1
  49. cista-0.5.0/cista/wwwroot/assets/signin-bbf26a1b.js → cista-0.7.0/cista/wwwroot/assets/signin-dcc16c88.js +1 -1
  50. cista-0.5.0/cista/wwwroot/assets/signout-caa34d68.js → cista-0.7.0/cista/wwwroot/assets/signout-935b6b65.js +1 -1
  51. cista-0.5.0/cista/wwwroot/assets/skip-423d5cf0.js → cista-0.7.0/cista/wwwroot/assets/skip-adeae5b8.js +1 -1
  52. cista-0.5.0/cista/wwwroot/assets/spinner-b299e14e.js → cista-0.7.0/cista/wwwroot/assets/spinner-7c5e1e66.js +1 -1
  53. cista-0.5.0/cista/wwwroot/assets/stop-91578a62.js → cista-0.7.0/cista/wwwroot/assets/stop-6ec4fac4.js +1 -1
  54. cista-0.5.0/cista/wwwroot/assets/trash-3b7b72a3.js → cista-0.7.0/cista/wwwroot/assets/trash-218fd3df.js +1 -1
  55. cista-0.5.0/cista/wwwroot/assets/triangle-724a2314.js → cista-0.7.0/cista/wwwroot/assets/triangle-40a425a9.js +1 -1
  56. cista-0.5.0/cista/wwwroot/assets/unfullscreen-29f4977c.js → cista-0.7.0/cista/wwwroot/assets/unfullscreen-3d51fed5.js +1 -1
  57. cista-0.5.0/cista/wwwroot/assets/up-arrow-ceb58d59.js → cista-0.7.0/cista/wwwroot/assets/up-arrow-af385124.js +1 -1
  58. cista-0.5.0/cista/wwwroot/assets/upload-cloud-936fb8b2.js → cista-0.7.0/cista/wwwroot/assets/upload-cloud-68d0fe9f.js +1 -1
  59. cista-0.5.0/cista/wwwroot/assets/user-cog-887c6f3f.js → cista-0.7.0/cista/wwwroot/assets/user-cog-bca0b085.js +1 -1
  60. cista-0.5.0/cista/wwwroot/assets/user-ab4ed9ac.js → cista-0.7.0/cista/wwwroot/assets/user-dd4fef53.js +1 -1
  61. cista-0.5.0/cista/wwwroot/assets/volume-high-74a17568.js → cista-0.7.0/cista/wwwroot/assets/volume-high-0f8f0e3d.js +1 -1
  62. cista-0.5.0/cista/wwwroot/assets/volume-low-f7170d5f.js → cista-0.7.0/cista/wwwroot/assets/volume-low-b1bd663e.js +1 -1
  63. cista-0.5.0/cista/wwwroot/assets/volume-medium-7b16c1db.js → cista-0.7.0/cista/wwwroot/assets/volume-medium-cd88f329.js +1 -1
  64. cista-0.5.0/cista/wwwroot/assets/volume-mute-0c7078a1.js → cista-0.7.0/cista/wwwroot/assets/volume-mute-458a2f48.js +1 -1
  65. cista-0.5.0/cista/wwwroot/assets/window-f7f79ada.js → cista-0.7.0/cista/wwwroot/assets/window-b63b3c5a.js +1 -1
  66. cista-0.5.0/cista/wwwroot/assets/window-cross-e3a75b33.js → cista-0.7.0/cista/wwwroot/assets/window-cross-b0bd1b60.js +1 -1
  67. cista-0.5.0/cista/wwwroot/assets/wordwrap-3bcce83c.js → cista-0.7.0/cista/wwwroot/assets/wordwrap-26b55346.js +1 -1
  68. cista-0.5.0/cista/wwwroot/assets/zoomin-bd27188c.js → cista-0.7.0/cista/wwwroot/assets/zoomin-cb10ed9f.js +1 -1
  69. cista-0.5.0/cista/wwwroot/assets/zoomout-31844707.js → cista-0.7.0/cista/wwwroot/assets/zoomout-90bcc471.js +1 -1
  70. {cista-0.5.0 → cista-0.7.0}/cista/wwwroot/index.html +3 -4
  71. {cista-0.5.0 → cista-0.7.0}/pyproject.toml +3 -0
  72. cista-0.5.0/cista/watching.py +0 -335
  73. cista-0.5.0/cista/wwwroot/assets/cog-fcdd928d.js +0 -1
  74. cista-0.5.0/cista/wwwroot/assets/index-6716899e.js +0 -11
  75. cista-0.5.0/cista/wwwroot/assets/index-bcee9add.css +0 -1
  76. {cista-0.5.0 → cista-0.7.0}/.gitignore +0 -0
  77. {cista-0.5.0 → cista-0.7.0}/cista/__init__.py +0 -0
  78. {cista-0.5.0 → cista-0.7.0}/cista/__main__.py +0 -0
  79. {cista-0.5.0 → cista-0.7.0}/cista/config.py +0 -0
  80. {cista-0.5.0 → cista-0.7.0}/cista/droppy.py +0 -0
  81. {cista-0.5.0 → cista-0.7.0}/cista/fileio.py +0 -0
  82. {cista-0.5.0 → cista-0.7.0}/cista/server80.py +0 -0
  83. {cista-0.5.0 → cista-0.7.0}/cista/session.py +0 -0
  84. {cista-0.5.0 → cista-0.7.0}/cista/util/__init__.py +0 -0
  85. {cista-0.5.0 → cista-0.7.0}/cista/util/asynclink.py +0 -0
  86. {cista-0.5.0 → cista-0.7.0}/cista/util/filename.py +0 -0
  87. {cista-0.5.0 → cista-0.7.0}/cista/util/lrucache.py +0 -0
  88. {cista-0.5.0 → cista-0.7.0}/cista/util/pwgen.py +0 -0
  89. {cista-0.5.0 → cista-0.7.0}/cista/wwwroot/assets/logo-97d1d7eb.svg +0 -0
  90. {cista-0.5.0 → cista-0.7.0}/cista/wwwroot/robots.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cista
3
- Version: 0.5.0
3
+ Version: 0.7.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
@@ -13,7 +13,10 @@ Requires-Dist: inotify
13
13
  Requires-Dist: msgspec
14
14
  Requires-Dist: natsort
15
15
  Requires-Dist: pathvalidate
16
+ Requires-Dist: pillow
17
+ Requires-Dist: pyav
16
18
  Requires-Dist: pyjwt
19
+ Requires-Dist: pymupdf
17
20
  Requires-Dist: sanic
18
21
  Requires-Dist: stream-zip
19
22
  Requires-Dist: tomli-w
@@ -24,9 +27,9 @@ Description-Content-Type: text/markdown
24
27
 
25
28
  # Cista Web Storage
26
29
 
27
- <img src="https://git.zi.fi/Vasanko/cista-storage/raw/branch/main/docs/cista.jpg" align=right width=250>
30
+ <img src="https://git.zi.fi/Vasanko/cista-storage/raw/branch/main/docs/cista.webp" align=left width=250>
28
31
 
29
- Cista takes its name from the ancient cistae, metal containers used by Greeks and Egyptians to safeguard valuable items. This modern application provides a browser interface for secure and accessible file storage, echoing the trust and reliability of its historical namesake.
32
+ Cista takes its name from the ancient *cistae*, metal containers used by Greeks and Egyptians to safeguard valuable items. This modern application provides a browser interface for secure and accessible file storage, echoing the trust and reliability of its historical namesake.
30
33
 
31
34
  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**.
32
35
 
@@ -70,7 +73,9 @@ To use your own TLS certificates, place them in the config folder and run:
70
73
  cista -l cista.example.com
71
74
  ```
72
75
 
73
- 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. Caddy configuration **/etc/caddy/Caddyfile** is dead simple:
76
+ 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.
77
+
78
+ `/etc/caddy/Caddyfile`:
74
79
 
75
80
  ```Caddyfile
76
81
  cista.example.com {
@@ -96,7 +101,7 @@ pip install -e '.[dev]'
96
101
  cista --dev -l :8000 /path/to/files
97
102
  ```
98
103
 
99
- We use `hatch shell` for installing on a virtual environment, to not disturb the rest of the system with our hacking.
104
+ We use `hatch shell` for installing on a virtual environment, to avoid disturbing the rest of the system with our hacking.
100
105
 
101
106
  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.
102
107
 
@@ -104,9 +109,9 @@ Vue is used to build files in `cista/wwwroot`, included prebuilt in the Python p
104
109
 
105
110
  This setup allows easy addition of storages, each with its own domain, configuration, and files.
106
111
 
107
- 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.
112
+ 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.
108
113
 
109
- Create **/etc/systemd/system/cista@.service**:
114
+ Create `/etc/systemd/system/cista@.service`:
110
115
 
111
116
  ```ini
112
117
  [Unit]
@@ -114,7 +119,7 @@ Description=Cista storage %i
114
119
 
115
120
  [Service]
116
121
  User=storage
117
- ExecStart=cista -c /srv/cista/%i -l /srv/cista/%i/socket /media/storage/@%i/
122
+ ExecStart=cista -c /srv/cista/%i -l /srv/cista/%i/socket /media/storage/%i
118
123
  Restart=always
119
124
 
120
125
  [Install]
@@ -129,9 +134,9 @@ systemctl enable --now cista@foo.example.com
129
134
  systemctl enable --now cista@bar.example.com
130
135
  ```
131
136
 
132
- Public exposure is easiest using the Caddy web server, but Nginx or others also work. Run the server with -l domain.example.com if you have TLS certificates in the config folder.
137
+ Public exposure is easiest using the Caddy web server.
133
138
 
134
- **/etc/caddy/Caddyfile**:
139
+ `/etc/caddy/Caddyfile`:
135
140
 
136
141
  ```Caddyfile
137
142
  foo.example.com, bar.example.com {
@@ -1,116 +1,118 @@
1
- # Cista Web Storage
2
-
3
- <img src="https://git.zi.fi/Vasanko/cista-storage/raw/branch/main/docs/cista.jpg" align=right width=250>
4
-
5
- Cista takes its name from the ancient cistae, metal containers used by Greeks and Egyptians to safeguard valuable items. This modern application provides a browser interface for secure and accessible file storage, echoing the trust and reliability of its historical namesake.
6
-
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
-
9
- 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
-
11
- 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.
12
-
13
- Experience Cista by visiting [Cista Demo](https://drop.zi.fi) for a test run and perhaps upload something...
14
-
15
-
16
- ## Getting Started
17
- ### Installation
18
-
19
- To install the cista application, use:
20
-
21
- ```fish
22
- pip install cista
23
- ```
24
-
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)
30
- ```fish
31
- cista --user yourname --privileged
32
- ```
33
-
34
- Serve your files at http://localhost:8000:
35
- ```fish
36
- cista -l :8000 /path/to/files
37
- ```
38
-
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
-
41
- ### Internet Access
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. Caddy configuration **/etc/caddy/Caddyfile** is dead simple:
50
-
51
- ```Caddyfile
52
- cista.example.com {
53
- reverse_proxy :8000
54
- }
55
- ```
56
-
57
- ## Development setup
58
-
59
- 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.
60
-
61
- ```fish
62
- cd frontend
63
- npm install
64
- npm run dev
65
- ```
66
-
67
- Concurrently, start the backend on another terminal:
68
-
69
- ```fish
70
- hatch shell
71
- pip install -e '.[dev]'
72
- cista --dev -l :8000 /path/to/files
73
- ```
74
-
75
- We use `hatch shell` for installing on a virtual environment, to not disturb the rest of the system with our hacking.
76
-
77
- 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.
78
-
79
- ## System Deployment
80
-
81
- This setup allows easy addition of storages, each with its own domain, configuration, and files.
82
-
83
- 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.
84
-
85
- Create **/etc/systemd/system/cista@.service**:
86
-
87
- ```ini
88
- [Unit]
89
- Description=Cista storage %i
90
-
91
- [Service]
92
- User=storage
93
- ExecStart=cista -c /srv/cista/%i -l /srv/cista/%i/socket /media/storage/@%i/
94
- Restart=always
95
-
96
- [Install]
97
- WantedBy=multi-user.target
98
- ```
99
-
100
- This setup supports multiple storages, each under `/media/storage/<domain>` for files and `/srv/cista/<domain>/` for configuration. UNIX sockets are used instead of numeric ports for convenience.
101
-
102
- ```fish
103
- systemctl daemon-reload
104
- systemctl enable --now cista@foo.example.com
105
- systemctl enable --now cista@bar.example.com
106
- ```
107
-
108
- Public exposure is easiest using the Caddy web server, but Nginx or others also work. Run the server with -l domain.example.com if you have TLS certificates in the config folder.
109
-
110
- **/etc/caddy/Caddyfile**:
111
-
112
- ```Caddyfile
113
- foo.example.com, bar.example.com {
114
- reverse_proxy unix//srv/cista/{host}/socket
115
- }
116
- ```
1
+ # Cista Web Storage
2
+
3
+ <img src="https://git.zi.fi/Vasanko/cista-storage/raw/branch/main/docs/cista.webp" align=left width=250>
4
+
5
+ Cista takes its name from the ancient *cistae*, metal containers used by Greeks and Egyptians to safeguard valuable items. This modern application provides a browser interface for secure and accessible file storage, echoing the trust and reliability of its historical namesake.
6
+
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
+
9
+ 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
+
11
+ 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.
12
+
13
+ Experience Cista by visiting [Cista Demo](https://drop.zi.fi) for a test run and perhaps upload something...
14
+
15
+
16
+ ## Getting Started
17
+ ### Installation
18
+
19
+ To install the cista application, use:
20
+
21
+ ```fish
22
+ pip install cista
23
+ ```
24
+
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)
30
+ ```fish
31
+ cista --user yourname --privileged
32
+ ```
33
+
34
+ Serve your files at http://localhost:8000:
35
+ ```fish
36
+ cista -l :8000 /path/to/files
37
+ ```
38
+
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
+
41
+ ### Internet Access
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.
50
+
51
+ `/etc/caddy/Caddyfile`:
52
+
53
+ ```Caddyfile
54
+ cista.example.com {
55
+ reverse_proxy :8000
56
+ }
57
+ ```
58
+
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.
80
+
81
+ ## System Deployment
82
+
83
+ This setup allows easy addition of storages, each with its own domain, configuration, and files.
84
+
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.
86
+
87
+ Create `/etc/systemd/system/cista@.service`:
88
+
89
+ ```ini
90
+ [Unit]
91
+ Description=Cista storage %i
92
+
93
+ [Service]
94
+ User=storage
95
+ ExecStart=cista -c /srv/cista/%i -l /srv/cista/%i/socket /media/storage/%i
96
+ Restart=always
97
+
98
+ [Install]
99
+ WantedBy=multi-user.target
100
+ ```
101
+
102
+ This setup supports multiple storages, each under `/media/storage/<domain>` for files and `/srv/cista/<domain>/` for configuration. UNIX sockets are used instead of numeric ports for convenience.
103
+
104
+ ```fish
105
+ systemctl daemon-reload
106
+ systemctl enable --now cista@foo.example.com
107
+ systemctl enable --now cista@bar.example.com
108
+ ```
109
+
110
+ Public exposure is easiest using the Caddy web server.
111
+
112
+ `/etc/caddy/Caddyfile`:
113
+
114
+ ```Caddyfile
115
+ foo.example.com, bar.example.com {
116
+ reverse_proxy unix//srv/cista/{host}/socket
117
+ }
118
+ ```
@@ -1,2 +1,2 @@
1
1
  # This file is automatically generated by hatch build.
2
- __version__ = '0.5.0'
2
+ __version__ = '0.7.0'
@@ -111,13 +111,24 @@ async def watch(req, ws):
111
111
  )
112
112
  uuid = token_bytes(16)
113
113
  try:
114
- with watching.state.lock:
115
- q = watching.pubsub[uuid] = asyncio.Queue()
116
- # Init with disk usage and full tree
117
- await ws.send(watching.format_space(watching.state.space))
118
- await ws.send(watching.format_root(watching.state.root))
114
+ q, space, root = await asyncio.get_event_loop().run_in_executor(
115
+ req.app.ctx.threadexec, subscribe, uuid, ws
116
+ )
117
+ await ws.send(space)
118
+ await ws.send(root)
119
119
  # Send updates
120
120
  while True:
121
121
  await ws.send(await q.get())
122
122
  finally:
123
123
  del watching.pubsub[uuid]
124
+
125
+
126
+ def subscribe(uuid, ws):
127
+ with watching.state.lock:
128
+ q = watching.pubsub[uuid] = asyncio.Queue()
129
+ # Init with disk usage and full tree
130
+ return (
131
+ q,
132
+ watching.format_space(watching.state.space),
133
+ watching.format_root(watching.state.root),
134
+ )
@@ -1,6 +1,7 @@
1
1
  import asyncio
2
2
  import datetime
3
3
  import mimetypes
4
+ import threading
4
5
  from concurrent.futures import ThreadPoolExecutor
5
6
  from pathlib import Path, PurePath, PurePosixPath
6
7
  from stat import S_IFDIR, S_IFREG
@@ -10,12 +11,12 @@ from wsgiref.handlers import format_date_time
10
11
  import brotli
11
12
  import sanic.helpers
12
13
  from blake3 import blake3
13
- from sanic import Blueprint, Sanic, empty, raw
14
+ from sanic import Blueprint, Sanic, empty, raw, redirect
14
15
  from sanic.exceptions import Forbidden, NotFound
15
- from sanic.log import logging
16
+ from sanic.log import logger
16
17
  from stream_zip import ZIP_AUTO, stream_zip
17
18
 
18
- from cista import auth, config, session, watching
19
+ from cista import auth, config, preview, session, watching
19
20
  from cista.api import bp
20
21
  from cista.util.apphelpers import handle_sanic_exception
21
22
 
@@ -24,6 +25,7 @@ sanic.helpers._ENTITY_HEADERS = frozenset()
24
25
 
25
26
  app = Sanic("cista", strict_slashes=True)
26
27
  app.blueprint(auth.bp)
28
+ app.blueprint(preview.bp)
27
29
  app.blueprint(bp)
28
30
  app.exception(Exception)(handle_sanic_exception)
29
31
 
@@ -31,14 +33,15 @@ app.exception(Exception)(handle_sanic_exception)
31
33
  @app.before_server_start
32
34
  async def main_start(app, loop):
33
35
  config.load_config()
34
- await watching.start(app, loop)
35
36
  app.ctx.threadexec = ThreadPoolExecutor(
36
37
  max_workers=8, thread_name_prefix="cista-ioworker"
37
38
  )
39
+ await watching.start(app, loop)
38
40
 
39
41
 
40
42
  @app.after_server_stop
41
43
  async def main_stop(app, loop):
44
+ quit.set()
42
45
  await watching.stop(app, loop)
43
46
  app.ctx.threadexec.shutdown()
44
47
 
@@ -122,7 +125,7 @@ def _load_wwwroot(www):
122
125
  if not wwwnew:
123
126
  msg = f"Web frontend missing from {base}\n Did you forget: hatch build\n"
124
127
  if not www:
125
- logging.warning(msg)
128
+ logger.warning(msg)
126
129
  if not app.debug:
127
130
  msg = "Web frontend missing. Cista installation is broken.\n"
128
131
  wwwnew[""] = (
@@ -141,7 +144,7 @@ def _load_wwwroot(www):
141
144
  async def start(app):
142
145
  await load_wwwroot(app)
143
146
  if app.debug:
144
- app.add_task(refresh_wwwroot())
147
+ app.add_task(refresh_wwwroot(), name="refresh_wwwroot")
145
148
 
146
149
 
147
150
  async def load_wwwroot(app):
@@ -151,27 +154,31 @@ async def load_wwwroot(app):
151
154
  )
152
155
 
153
156
 
157
+ quit = threading.Event()
158
+
159
+
154
160
  async def refresh_wwwroot():
155
- while True:
156
- await asyncio.sleep(0.5)
157
- try:
158
- wwwold = www
159
- await load_wwwroot(app)
160
- changes = ""
161
- for name in sorted(www):
162
- attr = www[name]
163
- if wwwold.get(name) == attr:
164
- continue
165
- headers = attr[2]
166
- changes += f"{headers['last-modified']} {headers['etag']} /{name}\n"
167
- for name in sorted(set(wwwold) - set(www)):
168
- changes += f"Deleted /{name}\n"
169
- if changes:
170
- print(f"Updated wwwroot:\n{changes}", end="", flush=True)
171
- except Exception as e:
172
- print("Error loading wwwroot", e)
173
- if not app.debug:
174
- return
161
+ try:
162
+ while not quit.is_set():
163
+ try:
164
+ wwwold = www
165
+ await load_wwwroot(app)
166
+ changes = ""
167
+ for name in sorted(www):
168
+ attr = www[name]
169
+ if wwwold.get(name) == attr:
170
+ continue
171
+ headers = attr[2]
172
+ changes += f"{headers['last-modified']} {headers['etag']} /{name}\n"
173
+ for name in sorted(set(wwwold) - set(www)):
174
+ changes += f"Deleted /{name}\n"
175
+ if changes:
176
+ print(f"Updated wwwroot:\n{changes}", end="", flush=True)
177
+ except Exception as e:
178
+ print(f"Error loading wwwroot: {e!r}")
179
+ await asyncio.sleep(0.5)
180
+ except asyncio.CancelledError:
181
+ pass
175
182
 
176
183
 
177
184
  @app.route("/<path:path>", methods=["GET", "HEAD"])
@@ -191,6 +198,12 @@ async def wwwroot(req, path=""):
191
198
  return raw(data, headers=headers)
192
199
 
193
200
 
201
+ @app.route("/favicon.ico", methods=["GET", "HEAD"])
202
+ async def favicon(req):
203
+ # Browsers keep asking for it when viewing files (not HTML with icon link)
204
+ return redirect("/assets/logo-97d1d7eb.svg", status=308)
205
+
206
+
194
207
  def get_files(wanted: set) -> list[tuple[PurePosixPath, Path]]:
195
208
  loc = PurePosixPath()
196
209
  idx = 0
@@ -251,7 +264,7 @@ async def zip_download(req, keys, zipfile, ext):
251
264
  for chunk in stream_zip(local_files(files)):
252
265
  asyncio.run_coroutine_threadsafe(queue.put(chunk), loop).result()
253
266
  except Exception:
254
- logging.exception("Error streaming ZIP")
267
+ logger.exception("Error streaming ZIP")
255
268
  raise
256
269
  finally:
257
270
  asyncio.run_coroutine_threadsafe(queue.put(None), loop)
@@ -71,7 +71,7 @@ def verify(request, *, privileged=False):
71
71
  raise Forbidden("Access Forbidden: Only for privileged users", quiet=True)
72
72
  elif config.config.public or request.ctx.user:
73
73
  return
74
- raise Unauthorized("Login required", "cookie", quiet=True)
74
+ raise Unauthorized(f"Login required for {request.path}", "cookie", quiet=True)
75
75
 
76
76
 
77
77
  bp = Blueprint("auth")
@@ -159,3 +159,35 @@ async def logout_post(request):
159
159
  res = json({"message": msg})
160
160
  session.delete(res)
161
161
  return res
162
+
163
+
164
+ @bp.post("/password-change")
165
+ async def change_password(request):
166
+ try:
167
+ if request.headers.content_type == "application/json":
168
+ username = request.json["username"]
169
+ pwchange = request.json["passwordChange"]
170
+ password = request.json["password"]
171
+ else:
172
+ username = request.form["username"][0]
173
+ pwchange = request.form["passwordChange"][0]
174
+ password = request.form["password"][0]
175
+ if not username or not password:
176
+ raise KeyError
177
+ except KeyError:
178
+ raise BadRequest(
179
+ "Missing username, passwordChange or password",
180
+ ) from None
181
+ try:
182
+ user = login(username, password)
183
+ set_password(user, pwchange)
184
+ except ValueError as e:
185
+ raise Forbidden(str(e), context={"redirect": "/login"}) from e
186
+
187
+ if "text/html" in request.headers.accept:
188
+ res = redirect("/")
189
+ session.flash(res, "Password updated")
190
+ else:
191
+ res = json({"message": "Password updated"})
192
+ session.create(res, username)
193
+ return res
@@ -0,0 +1,114 @@
1
+ import asyncio
2
+ import io
3
+ import mimetypes
4
+ import urllib.parse
5
+ from pathlib import PurePosixPath
6
+ from urllib.parse import unquote
7
+ from wsgiref.handlers import format_date_time
8
+
9
+ import av
10
+ import av.datasets
11
+ import fitz # PyMuPDF
12
+ from PIL import Image
13
+ from sanic import Blueprint, empty, raw
14
+ from sanic.exceptions import NotFound
15
+ from sanic.log import logger
16
+
17
+ from cista import config
18
+ from cista.util.filename import sanitize
19
+
20
+ bp = Blueprint("preview", url_prefix="/preview")
21
+
22
+
23
+ @bp.get("/<path:path>")
24
+ async def preview(req, path):
25
+ """Preview a file"""
26
+ maxsize = int(req.args.get("px", 1024))
27
+ maxzoom = float(req.args.get("zoom", 2.0))
28
+ quality = int(req.args.get("q", 40))
29
+ rel = PurePosixPath(sanitize(unquote(path)))
30
+ path = config.config.path / rel
31
+ stat = path.lstat()
32
+ etag = config.derived_secret(
33
+ "preview", rel, stat.st_mtime_ns, quality, maxsize, maxzoom
34
+ ).hex()
35
+ savename = PurePosixPath(path.name).with_suffix(".webp")
36
+ headers = {
37
+ "etag": etag,
38
+ "last-modified": format_date_time(stat.st_mtime),
39
+ "cache-control": "max-age=604800, immutable"
40
+ + ("" if config.config.public else ", private"),
41
+ "content-type": "image/webp",
42
+ "content-disposition": f"inline; filename*=UTF-8''{urllib.parse.quote(savename.as_posix())}",
43
+ }
44
+ if req.headers.if_none_match == etag:
45
+ # The client has it cached, respond 304 Not Modified
46
+ return empty(304, headers=headers)
47
+
48
+ if not path.is_file():
49
+ raise NotFound("File not found")
50
+
51
+ img = await asyncio.get_event_loop().run_in_executor(
52
+ req.app.ctx.threadexec, dispatch, path, quality, maxsize, maxzoom
53
+ )
54
+ return raw(img, headers=headers)
55
+
56
+
57
+ def dispatch(path, quality, maxsize, maxzoom):
58
+ if path.suffix.lower() in (".pdf", ".xps", ".epub", ".mobi"):
59
+ return process_pdf(path, quality=quality, maxsize=maxsize, maxzoom=maxzoom)
60
+ if mimetypes.guess_type(path.name)[0].startswith("video/"):
61
+ return process_video(path, quality=quality, maxsize=maxsize)
62
+ return process_image(path, quality=quality, maxsize=maxsize)
63
+
64
+
65
+ def process_image(path, *, maxsize, quality):
66
+ img = Image.open(path)
67
+ w, h = img.size
68
+ img.thumbnail((min(w, maxsize), min(h, maxsize)))
69
+ # Fix rotation based on EXIF data
70
+ try:
71
+ rotate_values = {3: 180, 6: 270, 8: 90}
72
+ orientation = img._getexif().get(274)
73
+ if orientation in rotate_values:
74
+ logger.debug(f"Rotating preview {path} by {rotate_values[orientation]}")
75
+ img = img.rotate(rotate_values[orientation], expand=True)
76
+ except AttributeError:
77
+ ...
78
+ except Exception as e:
79
+ logger.error(f"Error rotating preview image: {e}")
80
+ # Save as webp
81
+ imgdata = io.BytesIO()
82
+ img.save(imgdata, format="webp", quality=quality, method=4)
83
+ return imgdata.getvalue()
84
+
85
+
86
+ def process_pdf(path, *, maxsize, maxzoom, quality, page_number=0):
87
+ pdf = fitz.open(path)
88
+ page = pdf.load_page(page_number)
89
+ w, h = page.rect[2:4]
90
+ zoom = min(maxsize / w, maxsize / h, maxzoom)
91
+ mat = fitz.Matrix(zoom, zoom)
92
+ pix = page.get_pixmap(matrix=mat)
93
+ return pix.pil_tobytes(format="webp", quality=quality, method=4)
94
+
95
+
96
+ def process_video(path, *, maxsize, quality):
97
+ with av.open(str(path)) as container:
98
+ stream = container.streams.video[0]
99
+ rotation = (
100
+ stream.side_data
101
+ and stream.side_data.get(av.stream.SideData.DISPLAYMATRIX)
102
+ or 0
103
+ )
104
+ stream.codec_context.skip_frame = "NONKEY"
105
+ container.seek(container.duration // 8)
106
+ frame = next(container.decode(stream))
107
+ img = frame.to_image()
108
+
109
+ img.thumbnail((maxsize, maxsize))
110
+ imgdata = io.BytesIO()
111
+ if rotation:
112
+ img = img.rotate(rotation, expand=True)
113
+ img.save(imgdata, format="webp", quality=quality, method=4)
114
+ return imgdata.getvalue()