Compare commits

...

161 commits

Author SHA1 Message Date
68996acd54 update TODOs (again) 2023-05-04 11:55:43 +02:00
768c8c60b8 update TODOs 2023-05-04 10:47:08 +02:00
5002f11bf3 update readme 2023-05-03 22:42:56 +02:00
808ffd4964 fix some warnings 2023-03-10 08:39:24 +01:00
1f69a77a03 update Cargo.lock 2023-03-10 08:23:56 +01:00
f3ca7e3328 bump version to 2.0.1 2023-03-10 08:22:16 +01:00
42aceb2a01 Fix error codes, handle bad requests for creation
Fix that invalid UTF-8 or continuing the read mid-character crashes
server thread.
2023-03-10 08:18:11 +01:00
1652a850b8 add nix flake installation guide 2023-03-09 20:44:38 +01:00
181ebb3a63 Fix XSS attack (again)
Now escaping only for slashes, since HTML is apparently case insensitive and using a script closing tag that wasn't entirely lowercase bypassed the earlier fix.
2023-03-09 20:05:57 +01:00
c83a775ac2 add direnv directory to gitignore 2023-03-09 20:05:23 +01:00
e3a5527a8c README: reorganize TODOs to roadmap 2023-03-06 12:12:47 +01:00
5d0f73a5f3 flake.lock: Update
Flake lock file updates:

• Updated input 'naersk/nixpkgs':
    'github:NixOS/nixpkgs/8c66bd1b68f4708c90dcc97c6f7052a5a7b33257' (2023-02-16)
  → 'github:NixOS/nixpkgs/f5ffd5787786dde3a8bf648c7a1b5f78c4e01abb' (2023-03-03)
• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/8c66bd1b68f4708c90dcc97c6f7052a5a7b33257' (2023-02-16)
  → 'github:NixOS/nixpkgs/f5ffd5787786dde3a8bf648c7a1b5f78c4e01abb' (2023-03-03)
2023-03-05 19:35:07 +01:00
c5b0a8ef79 docker! 2023-03-05 17:02:18 +01:00
75755052c3 removed keyword 2023-03-05 16:10:19 +01:00
01eb19e732 removed unnecessary import 2023-03-05 16:08:26 +01:00
91568a1590 updated readme 2023-03-05 16:07:08 +01:00
8039fe75a2 fixed gitignore result ignore 2023-03-05 15:44:28 +01:00
86e41865bb ignored result 2023-03-05 15:43:49 +01:00
b313b5ce73 updated readme 2023-03-05 15:37:48 +01:00
f1de42e5c0 updated readme a bit 2023-03-05 15:23:11 +01:00
e4575d7d6e rebrand + added dockerfile agian 2023-03-05 15:21:12 +01:00
d1c583d4b0 ideas 2023-03-04 01:10:16 +01:00
8623bcb9ae fixed some links 2023-03-04 01:02:17 +01:00
353cb5dfde removed attempt at rewriting in other template system, it's not that important for now 2023-03-04 00:28:57 +01:00
1f2589976d templating rewrite start 2023-03-03 23:52:16 +01:00
3f2bdfdaed added markup crate 2023-02-27 09:55:16 +01:00
27fe305640 added that if you click the logo you get to / 2023-02-27 00:55:27 +01:00
1ebbe5d922 added embedded images at pasta view 2023-02-27 00:47:40 +01:00
51751e3ee2 added customizability for /pasta/, /url/ and /raw/ endpoints 2023-02-27 00:18:00 +01:00
f352129c78 removed light mode 2023-02-22 21:45:39 +01:00
8db35b80d9 Update README.md with a new idea! 2023-02-19 00:13:20 +00:00
56332c61f3 small changes to info thing 2023-02-19 00:46:26 +01:00
fa31a0a51a more todos! 2023-02-18 00:53:37 +01:00
1069d4c676 added more todos 2023-02-18 00:40:58 +01:00
7bfebb27d9 updated readme and cargo stuff 2023-02-18 00:02:28 +01:00
7abb3c5d11 added support for use of a custom names file 2023-02-17 23:27:39 +01:00
7d5c70ddd6 Some documentation and cleanup of old, commented out code.
Signed-off-by: Jade <obsidianical@gmail.com>
2023-02-17 22:32:21 +01:00
528a7b6899 cleanup of various clippy lints, bad practices and simplificaiton of some code
Signed-off-by: Jade <obsidianical@gmail.com>
2023-02-17 22:14:43 +01:00
fa67edc8c5 Documented microbin args
Signed-off-by: Jade <obsidianical@gmail.com>
2023-02-17 22:14:08 +01:00
39881a036a readme updates 2023-02-17 19:25:29 +01:00
57fd472eda changes like
- xss vuln
- docker shit
+ nix shit
2023-02-17 11:28:09 +01:00
Daniel Szabo
84136f1106 Merge branch 'master' of https://github.com/szabodanika/microbin 2022-11-14 21:43:14 +02:00
Daniel Szabo
ba784da74e Merge branch 'pr/95' 2022-11-14 21:42:35 +02:00
Dániel Szabó
0a80ac1359
Merge pull request #101 from albocc/feature/Add_3_days_expiration_option
Added expiration option for 3 days
2022-11-14 21:30:34 +02:00
Dániel Szabó
6564499c98
Merge pull request #104 from alex3305/cleanup-readme
Clean up README
2022-11-14 21:28:21 +02:00
Dániel Szabó
4fa2cc2a53
Merge branch 'master' into cleanup-readme 2022-11-14 21:28:12 +02:00
Dániel Szabó
4279653ca9
Update README.MD 2022-11-14 21:24:08 +02:00
Alex van den Hoogen
53326b0435
Clean up README
* microbin.eu reference was not working
* Consistent MicroBin spelling
* Link to LICENSE
* Clean up whitespace
* Insert codeblocks
* Set heading 2 instead of heading 3 (as h1 was already used by the title, next in line would be h2)
* Set monospace for some references
2022-11-09 19:55:13 +01:00
figsoda
68f4081745 fix typos 2022-11-09 10:46:51 -05:00
figsoda
089bb95c4f cargo fmt 2022-11-09 10:45:53 -05:00
figsoda
5f05206891
Merge pull request #1 from kwiniarski97/patch-1
doc: fixed invalid link to main website in readme
2022-11-09 10:45:32 -05:00
albocc
4fcd4e9e19 Added expiration option for 3 days 2022-11-09 11:57:49 +01:00
Karol Winiarski
89f902f99f
doc: fixed invalid link to main website
Link to a resource outside the repo itself should be prepended with https.
2022-11-09 10:26:44 +01:00
figsoda
958466818b apply clippy suggestions 2022-11-08 16:30:16 -05:00
figsoda
f41c2eb66b rename README.MD to README.md 2022-11-08 16:22:42 -05:00
Daniel Szabo
76cfc906ef Update docker.yml
Fix out of memory error on GH Actions build and automate GitHub Release with artifacts ( #47). See https://github.com/docker/build-push-action/issues/654#issuecomment-1285190151 and https://github.com/docker/build-push-action/issues/621
2022-11-08 11:46:11 +02:00
Daniel Szabo
66f6e0e46f Update docker.yml
Fix out of memory error on GH Actions build and automate GitHub Release with artifacts ( #47). See https://github.com/docker/build-push-action/issues/654#issuecomment-1285190151 and https://github.com/docker/build-push-action/issues/621
2022-11-08 00:23:17 +02:00
Dániel Szabó
4362d934e3
Merge pull request #85 from szabodanika/szabodanika
Merge personal branch to master
2022-11-07 20:34:14 +02:00
Daniel Szabo
d7f0f9637d Update build date - v1.2.0 is ready 2022-11-07 20:33:33 +02:00
Daniel Szabo
edd46eae58 Improved /pasta and /pastalist UX
- adjusted table column widths so buttons dont wrap into next row
- added Open button to url pastas so clicking on their keys redirect to their /pasta view, consistent with the rest of the pastas
- added copy redirect button to /pasta view of url pastas to make it easier to get the /url endpoint url
2022-11-07 20:30:33 +02:00
Daniel Szabo
c6e2b026e6 Improved QR code view
- added backlink to pasta page
- added link on QR SVG to its destination
- 404 if incorrect id
- QR code of URL pasta will now redirect to /url endpoint
2022-11-07 20:28:45 +02:00
Daniel Szabo
5854572e87 Reduced QR code size to fit phone screens 2022-11-07 20:26:44 +02:00
Daniel Szabo
5e1fcff979 Fixed a bug that caused url pastas to become plain text pastas 2022-11-07 20:25:42 +02:00
Daniel Szabo
ca1cd91635 Removed logo from README.MD
Because GitHub does not render HTML in Markdown files (correctly anyway)
2022-11-07 17:59:47 +02:00
Daniel Szabo
dac9ae8385 Moved FUNDING.yml out of workflows folder 2022-11-07 17:58:03 +02:00
Daniel Szabo
719691dfac Update README.MD
- Moved documentation to website
- Updated screenshots
- Added logo
- Reorganised feature list
- Added licence at the bottom
- Moved screenshot to .git folder
2022-11-07 17:57:50 +02:00
Daniel Szabo
b9a6717ba0 Add drag-and-drop to new file attachment button 2022-11-07 00:30:17 +02:00
Daniel Szabo
5e2513eb1f Fix empty pasta content showing code field
content has to be "No Text Content" to hide the code field on the pasta view
2022-11-07 00:27:25 +02:00
Daniel Szabo
7522d41919 Fix #88
Apparently if a multipart field contains multiple dashes, it will be read in multiple chunks and these chunks should be concatenated in order to read the entire field correctly.
2022-11-06 23:47:56 +02:00
Daniel Szabo
c6e5c6f018 Merge branch 'master' into szabodanika 2022-11-06 23:21:45 +02:00
Dániel Szabó
6a0c6b736d
Merge pull request #86 from henry40408/feature/issue-54
hashids
2022-11-06 22:58:46 +02:00
Daniel Szabo
2198cbdff9 Getting ready for 1.2 & new site release
- improved support for serving static resources from the binary, now supporting images
- added new logo
- changed save button
- fixed footer attribution text, it is not true anymore that MicroBin is made by myself
- replaced footer GitHub link with microbin.eu link
2022-11-01 20:56:07 +02:00
Heng-Yi Wu
b5da40fbdc
feat: hashids 2022-11-01 21:19:54 +08:00
Daniel Szabo
44b55ae08e Getting ready for 1.2.0 release: many smaller requests implemented
- Implements #7
- Implements #42 and therefore #64
- Improved #53
- Implements #59
- Implements #61
- Implements #63
- Implements #80
- Implements #84
- Added Info page
- Removed Help page
- Bumped version number to 1.2.0
- Fixed a bug where wide mode was still 720px wide
- Created FUNDING.yml
- Reorganised arguments in README.MD and documented new options
- Updated SECURITY.MD
- Added display of last read time and read count
- Increased default width to 800px to make UI less cluttered
- Reorganised index page
- New, better attach file button

I want to spend some time testing these changes and let everyone have a look at them before tagging and releasing new artifacts.
2022-10-29 14:11:55 +03:00
Daniel Szabo
769901c895 Added copy button for URL redirects
Fixes #80
2022-10-27 17:23:39 +03:00
Dániel Szabó
d2e7234d96 small ui improvements
- fix width for pure html mode
- improve copy button look and placement
- make input field heights more consistent on pasta creation page
2022-10-27 14:12:11 +03:00
Daniel Szabo
e258bcc2bd
added missing bracket in args.rs 2022-10-25 12:51:59 +03:00
Dániel Szabó
dc2c7094a8
Merge pull request #78 from HeapUnderfl0w/public-path
Public Url for Paths
2022-10-25 12:24:33 +03:00
HeapUnderflow
43061699f5
Merge branch 'master' into public-path 2022-10-24 12:57:21 +02:00
HeapUnderflow
4980d72df2
Add example to README.MD 2022-10-24 12:55:09 +02:00
Dániel Szabó
2e981b3128
Merge pull request #68 from hay-kot/chores/gitignore
Add gitignore
2022-10-22 21:46:37 +03:00
Dániel Szabó
3e58ba325a
Merge pull request #70 from hay-kot/feat/implement-copy-button
feat: add copy button to viewer
2022-10-22 21:42:51 +03:00
Hayden
b1ccb43855 remove hash 2022-10-22 10:37:43 -08:00
Hayden
fd8a66bcbc use proper semantics for a tag as button 2022-10-22 10:34:39 -08:00
Hayden
e031ea0e95 use a tag instead of button 2022-10-22 10:30:16 -08:00
Dániel Szabó
ec4d764f5a
Merge pull request #71 from hay-kot/feat/add-site-favicon
feat: add site favicon
2022-10-22 21:15:06 +03:00
Hayden
e8e21d561e remove icon and change styles 2022-10-22 10:12:15 -08:00
Hayden
487de0fcf7 use gray favicon 2022-10-22 10:02:52 -08:00
Hayden
7f4f784f2b add pasta_data 2022-10-22 09:55:11 -08:00
Dániel Szabó
52d87652ea
Merge pull request #69 from hay-kot/docker/expose-port
Expose port 8080 in dockerfile
2022-10-22 20:49:44 +03:00
Dániel Szabó
bc26fe87a5
Merge pull request #72 from hay-kot/feat/disable-file-attachment
feat: disable file attachment
2022-10-22 20:39:30 +03:00
Dániel Szabó
4926c578ec
Merge pull request #73 from stavros-k/patch-1
Fix --footer-text reference
2022-10-22 19:39:28 +03:00
HeapUnderflow
cfd494f80d
Do not append a second slash when no public path is set 2022-10-12 17:42:42 +02:00
HeapUnderflow
a404f9a997
Allow setting a public url for all paths 2022-10-12 17:13:21 +02:00
Stavros Kois
e786fa5a22
Fix --footer-text reference 2022-10-03 00:36:29 +03:00
Hayden
c39b778234 properly escape content 2022-10-01 20:50:05 -08:00
Hayden
0b5dea5dd1 add flag to docs 2022-09-30 22:12:24 -08:00
Hayden
82c30ce6cd conditionally render file upload 2022-09-30 22:11:25 -08:00
Hayden
fc3998243b ignore file processing if no_file_upload is true 2022-09-30 22:11:03 -08:00
Hayden
e17b26994f add cli args to disable file upload 2022-09-30 22:10:43 -08:00
Hayden
ef5d07392b add in template and as static resource 2022-09-30 21:52:08 -08:00
Hayden
cc504f781e add favicon resource 2022-09-30 21:51:52 -08:00
Hayden
1e8b17bb89 add copy button to viewer 2022-09-30 21:37:26 -08:00
Hayden
8223dd4973 export port 8080 in dockerfile 2022-09-30 21:11:16 -08:00
Hayden
6cea6262b8 init git ignore 2022-09-30 21:08:02 -08:00
Dániel Szabó
3ca89291dc
Merge pull request #57 from frdmn/patch-1
Fix Docker repository link in README to allow non-authenticated Docker Hub users.
2022-09-27 21:23:24 +03:00
Jonas Friedmann
2322c6713e
Fix Docker repository link 2022-09-27 20:09:44 +02:00
Dániel Szabó
05ad1d46c1
Update README.MD
Updated docker volume path according to 08871e1. Fixes #55
2022-09-27 16:19:41 +00:00
Dániel Szabó
d36472bcac
Update README.MD 2022-09-26 12:10:01 +00:00
Dániel Szabó
ff28faa222
Update README.MD 2022-09-26 11:53:54 +00:00
Dániel Szabó
60aac1aea7
Merge pull request #49 from arghyadipchak/master
Automated docker build for DockerHub + updated cargo.toml
2022-09-26 10:48:40 +03:00
Dániel Szabó
afa3c516ee
Merge branch 'master' into master 2022-09-26 10:35:38 +03:00
Dániel Szabó
7cd136cd7b
Merge pull request #50 from techiall/fix/dockerfile
Replace base docker image with Bitnami Minideb to fix #50 and #39
2022-09-16 19:37:57 +03:00
techial
a593ea0160
Update runing image 2022-08-28 16:27:38 +08:00
techial
5ae641fda0 Update Dockerfile 2022-08-26 22:25:59 +08:00
Arghyadip Chakraborty
08871e15b6 Automated docker build 2022-08-25 22:25:03 +05:30
Dániel Szabó
f55a5eba96
Merge pull request #43 from FoxFromDarkness66/patch-3
Update AUR installation method
2022-08-06 22:55:15 +01:00
FixFromDarkness
011cc25490
Update AUR installation method 2022-08-05 12:25:05 +03:00
Dániel Szabó
d44a3081bc
Delete FUNDING.yml 2022-08-02 22:36:06 +01:00
Dániel Szabó
51f7f54be7
Create SECURITY.md 2022-08-01 10:07:42 +01:00
Daniel Szabo
a3fc97a460 bump version number 2022-07-31 22:06:41 +01:00
Daniel Szabo
05941f0d6f Isolate pasta uploads from database.json by moving them into pasta_data/public/ 2022-07-31 21:49:36 +01:00
Daniel Szabo
7b4cd7c26e Implement upload filename sanitisation 2022-07-31 21:31:35 +01:00
Daniel Szabo
f54d5bd780 Add pasta ID + pasta URL to pasta page. Fix #36 2022-07-31 19:41:19 +01:00
Daniel Szabo
435c07d75e Implement manual deletion behaviour and fix #35 2022-07-31 19:18:07 +01:00
Dániel Szabó
cc09d1b529
Merge pull request #38 from uniqueNullptr2/uniqueNullptr2-fix-title-in-template
fix usage of title arg in template
2022-07-31 18:59:37 +01:00
uniqueNullptr2
60c3a1f9ac fix usage of title arg in template 2022-07-25 15:45:07 +02:00
Dániel Szabó
d4d94b61da
Merge pull request #34 from uniqueNullptr2/uniquenullptr2-add-config-from-env
add configuration from env to all clap options
2022-07-25 14:04:26 +01:00
Dániel Szabó
9053211904
Merge pull request #31 from dvdsk/file-size
Adds file size to pasta with an attachment
2022-07-25 13:39:00 +01:00
uniqueNullptr2
35a512680c fix mistype of syntax highlight option 2022-07-20 09:13:31 +02:00
uniqueNullptr2
bcd620ed43 add configuration from env to all clap options 2022-07-20 08:50:23 +02:00
Dániel Szabó
556f4e87df
Merge pull request #33 from amnesiacsardine/patch-1
Update to README.MD regarding Dockerfile
2022-07-18 00:05:56 +01:00
amnesiacsardine
fa88bce917
Update README.MD
Added how to pass command line arguments in the Dockerfile
2022-07-16 13:22:31 +02:00
Dániel Szabó
a5d326b679
Merge pull request #30 from dvdsk/master
Fixes #29 and displays pasta list in local timezone
2022-07-15 22:51:26 +01:00
Dániel Szabó
465873e095
Merge pull request #28 from FoxFromDarkness66/patch-2
Add bind address CL option
2022-07-15 22:47:39 +01:00
dvdsk
39233e9447
fixes #6 adding the size of the attached file 2022-07-14 01:08:13 +02:00
dvdsk
738e036cb5
pasta times are in systems local timezone 2022-07-13 23:55:28 +02:00
dvdsk
de2cc48f88
fixes #29 (time formating) 2022-07-13 23:54:48 +02:00
Dániel Szabó
0687f44137
Merge pull request #27 from FoxFromDarkness66/patch-1
Add AUR installation method
2022-07-07 23:12:48 +01:00
FixFromDarkness
fec933c5ec * Revert default address to 0.0.0.0 due to docker usage & compatibility
* Add --bind option to readme & change some examples
2022-07-07 20:08:29 +03:00
FixFromDarkness
cc2dd1e1fe * Add --bind option
* Changed default bind address to localhost
* Fix wrong log text about binding address
2022-07-07 19:45:31 +03:00
FixFromDarkness
85ed1b2b92
Add AUR installation method
AUR is Arch User Repository, "community-driven repository for Arch users. It contains package descriptions (PKGBUILDs) that allow you to compile a package from source with makepkg and then install it via pacman." 
I've made an AUR package that will allow any arch-based distro user to install and update microban to the latest version without manual version cheking&compiling. It will be easier for them to find it if you add information about this in the readme
2022-07-07 19:15:26 +03:00
Dániel Szabó
73ec59ccda
Merge pull request #21 from Arizard/master
Updates Dockerfile and adds docker build instructions
2022-06-27 20:58:01 +01:00
Arie
f5b9036a2a Uses volume absolute path, changes wording 2022-06-27 10:52:37 +10:00
Arie
0d43f2f60a Updates README with docker build instructions 2022-06-27 10:39:32 +10:00
Arie
aa9246da4e Removes COPY instruction for static directory 2022-06-27 10:37:25 +10:00
Dániel Szabó
1c21943c75
Merge pull request #17 from dvdsk/patch-1
Remove --linenumbers in systemd example
2022-06-05 19:30:05 +01:00
David Kleingeld
7f4b9f4aee
Remove --linenumbers in systemd example 2022-06-05 19:23:28 +02:00
Dániel Szabó
acd547dbf3
Update FUNDING.yml 2022-06-05 01:41:09 +01:00
Daniel Szabo
ce8bd4dd02 Added cargo information and readme badges 2022-06-05 00:20:23 +01:00
Dániel Szabó
81bf17e004
Merge pull request #16 from szabodanika/add_render.yaml
Add render.com deployment support
2022-06-05 00:02:44 +01:00
Dániel Szabó
92a29a02e5
Update render.yaml 2022-06-04 23:30:39 +01:00
Daniel Szabo
8b1702365c Add "Deploy to Render" button to README.MD 2022-06-04 23:05:47 +01:00
Daniel Szabo
291438e771 Update render.yaml 2022-06-04 22:55:16 +01:00
Daniel Szabo
497a8ef0e3 Add render.yaml 2022-06-04 22:51:09 +01:00
Daniel Szabo
ff921dc103 Additional changes to prev. commit 2022-06-04 22:21:22 +01:00
Daniel Szabo
fe013d9d85 Merge branch 'master' of https://github.com/szabodanika/microbin 2022-06-04 21:52:27 +01:00
Daniel Szabo
be3ac27920 Quick patch:
- Changed 302 responses to 200 where needed
- Fixed a bug where expiring pastas cause MicroBin to crahs
- Fixed a bug where water.css didn't have the correct MIME type
- Fixed a bug where missing doctype declaration caused styling issues in Firefox
2022-06-04 21:50:34 +01:00
Dániel Szabó
2f13a0e8e7
Create FUNDING.yml 2022-06-04 17:03:14 +01:00
Daniel Szabo
35d4df2cb8 Fixed a bug where opening private pastas would crash MicroBin 2022-06-03 18:11:14 +01:00
Daniel Szabo
4cc737731a Multiple enhancements and bugfixes
!Breaking change! - The updated version will not be able to read your old database file

Major improvements:
- Added editable pastas
- Added private pastas
- Added line numbers
- Added support for wide mode (1080p instead of 720p)
- Added syntax highlighting support
- Added read-only mode
- Added built-in help page
- Added option to remove logo, change title and footer text

Minor improvements:
- Improved looks in pure html mode
- Removed link to GitHub repo from navbar
- Broke up 7km long main.rs file into smaller modules
- Moved water.css into a template instead of serving it as an external resource
- Made Save button a bit bigger
- Updated README.MD

Bugfixes:
- Fixed a bug where an incorrect animal ID in a request would cause a crash
- Fixed a bug where an empty or corrupt JSON database would cause a crash
2022-06-03 17:24:34 +01:00
52 changed files with 5979 additions and 664 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

4
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,4 @@
# These are supported funding model platforms
github: szabodanika
ko_fi: dani_sz

BIN
.github/index.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
.github/logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

13
.github/workflows/build_nix.yml vendored Normal file
View file

@ -0,0 +1,13 @@
name: "Build legacy Nix package on Ubuntu"
on:
push:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: cachix/install-nix-action@v12
- name: Building package
run: nix-build . -A defaultPackage.x86_64-linux

192
.github/workflows/docker.yml vendored Normal file
View file

@ -0,0 +1,192 @@
name: Release
on:
push:
tags:
- v[0-9]+.[0-9]+.[0-9]+*
jobs:
release:
name: Publish to Github Relases
outputs:
rc: ${{ steps.check-tag.outputs.rc }}
strategy:
matrix:
include:
- target: aarch64-unknown-linux-musl
os: ubuntu-latest
use-cross: true
cargo-flags: ""
- target: aarch64-apple-darwin
os: macos-latest
use-cross: true
cargo-flags: ""
- target: aarch64-pc-windows-msvc
os: windows-latest
use-cross: true
cargo-flags: "--no-default-features"
- target: x86_64-apple-darwin
os: macos-latest
cargo-flags: ""
- target: x86_64-pc-windows-msvc
os: windows-latest
cargo-flags: ""
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
use-cross: true
cargo-flags: ""
- target: i686-unknown-linux-musl
os: ubuntu-latest
use-cross: true
cargo-flags: ""
- target: i686-pc-windows-msvc
os: windows-latest
use-cross: true
cargo-flags: ""
- target: armv7-unknown-linux-musleabihf
os: ubuntu-latest
use-cross: true
cargo-flags: ""
- target: arm-unknown-linux-musleabihf
os: ubuntu-latest
use-cross: true
cargo-flags: ""
- target: mips-unknown-linux-musl
os: ubuntu-latest
use-cross: true
cargo-flags: "--no-default-features"
- target: mipsel-unknown-linux-musl
os: ubuntu-latest
use-cross: true
cargo-flags: "--no-default-features"
- target: mips64-unknown-linux-gnuabi64
os: ubuntu-latest
use-cross: true
cargo-flags: "--no-default-features"
- target: mips64el-unknown-linux-gnuabi64
os: ubuntu-latest
use-cross: true
cargo-flags: "--no-default-features"
runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v2
- name: Check Tag
id: check-tag
shell: bash
run: |
tag=${GITHUB_REF##*/}
echo "::set-output name=version::$tag"
if [[ "$tag" =~ [0-9]+.[0-9]+.[0-9]+$ ]]; then
echo "::set-output name=rc::false"
else
echo "::set-output name=rc::true"
fi
- name: Install Rust Toolchain Components
uses: actions-rs/toolchain@v1
with:
override: true
target: ${{ matrix.target }}
toolchain: stable
profile: minimal # minimal component installation (ie, no documentation)
- name: Show Version Information (Rust, cargo, GCC)
shell: bash
run: |
gcc --version || true
rustup -V
rustup toolchain list
rustup default
cargo -V
rustc -V
- name: Build
uses: actions-rs/cargo@v1
with:
use-cross: ${{ matrix.use-cross }}
command: build
args: --locked --release --target=${{ matrix.target }} ${{ matrix.cargo-flags }}
- name: Build Archive
shell: bash
id: package
env:
target: ${{ matrix.target }}
version: ${{ steps.check-tag.outputs.version }}
run: |
set -euxo pipefail
bin=${GITHUB_REPOSITORY##*/}
src=`pwd`
dist=$src/dist
name=$bin-$version-$target
executable=target/$target/release/$bin
if [[ "$RUNNER_OS" == "Windows" ]]; then
executable=$executable.exe
fi
mkdir $dist
cp $executable $dist
cd $dist
if [[ "$RUNNER_OS" == "Windows" ]]; then
archive=$dist/$name.zip
7z a $archive *
echo "::set-output name=archive::`pwd -W`/$name.zip"
else
archive=$dist/$name.tar.gz
tar czf $archive *
echo "::set-output name=archive::$archive"
fi
- name: Publish Archive
uses: softprops/action-gh-release@v0.1.5
if: ${{ startsWith(github.ref, 'refs/tags/') }}
with:
draft: false
files: ${{ steps.package.outputs.archive }}
prerelease: ${{ steps.check-tag.outputs.rc == 'true' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
docker:
name: Publish to Docker Hub
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
needs: release
steps:
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ secrets.DOCKERHUB_REPO }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
with:
build-args: |
REPO=${{ github.repository }}
VER=${{ github.ref_name }}
platforms: |
linux/amd64
linux/arm64
push: ${{ github.ref_type == 'tag' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

154
.github/workflows/gh-release.yml vendored Normal file
View file

@ -0,0 +1,154 @@
name: GitHub Release
on:
push:
tags:
- v[0-9]+.[0-9]+.[0-9]+*
jobs:
release:
name: Publish to Github Releases
outputs:
rc: ${{ steps.check-tag.outputs.rc }}
strategy:
matrix:
include:
- target: aarch64-unknown-linux-musl
os: ubuntu-latest
use-cross: true
cargo-flags: ""
- target: aarch64-apple-darwin
os: macos-latest
use-cross: true
cargo-flags: ""
- target: aarch64-pc-windows-msvc
os: windows-latest
use-cross: true
cargo-flags: "--no-default-features"
- target: x86_64-apple-darwin
os: macos-latest
cargo-flags: ""
- target: x86_64-pc-windows-msvc
os: windows-latest
cargo-flags: ""
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
use-cross: true
cargo-flags: ""
- target: i686-unknown-linux-musl
os: ubuntu-latest
use-cross: true
cargo-flags: ""
- target: i686-pc-windows-msvc
os: windows-latest
use-cross: true
cargo-flags: ""
- target: armv7-unknown-linux-musleabihf
os: ubuntu-latest
use-cross: true
cargo-flags: ""
- target: arm-unknown-linux-musleabihf
os: ubuntu-latest
use-cross: true
cargo-flags: ""
- target: mips-unknown-linux-musl
os: ubuntu-latest
use-cross: true
cargo-flags: "--no-default-features"
- target: mipsel-unknown-linux-musl
os: ubuntu-latest
use-cross: true
cargo-flags: "--no-default-features"
- target: mips64-unknown-linux-gnuabi64
os: ubuntu-latest
use-cross: true
cargo-flags: "--no-default-features"
- target: mips64el-unknown-linux-gnuabi64
os: ubuntu-latest
use-cross: true
cargo-flags: "--no-default-features"
runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v2
- name: Check Tag
id: check-tag
shell: bash
run: |
tag=${GITHUB_REF##*/}
echo "::set-output name=version::$tag"
if [[ "$tag" =~ [0-9]+.[0-9]+.[0-9]+$ ]]; then
echo "::set-output name=rc::false"
else
echo "::set-output name=rc::true"
fi
- name: Install Rust Toolchain Components
uses: actions-rs/toolchain@v1
with:
override: true
target: ${{ matrix.target }}
toolchain: stable
profile: minimal # minimal component installation (ie, no documentation)
- name: Show Version Information (Rust, cargo, GCC)
shell: bash
run: |
gcc --version || true
rustup -V
rustup toolchain list
rustup default
cargo -V
rustc -V
- name: Build
uses: actions-rs/cargo@v1
with:
use-cross: ${{ matrix.use-cross }}
command: build
args: --locked --release --target=${{ matrix.target }} ${{ matrix.cargo-flags }}
- name: Build Archive
shell: bash
id: package
env:
target: ${{ matrix.target }}
version: ${{ steps.check-tag.outputs.version }}
run: |
set -euxo pipefail
bin=${GITHUB_REPOSITORY##*/}
src=`pwd`
dist=$src/dist
name=$bin-$version-$target
executable=target/$target/release/$bin
if [[ "$RUNNER_OS" == "Windows" ]]; then
executable=$executable.exe
fi
mkdir $dist
cp $executable $dist
cd $dist
if [[ "$RUNNER_OS" == "Windows" ]]; then
archive=$dist/$name.zip
7z a $archive *
echo "::set-output name=archive::`pwd -W`/$name.zip"
else
archive=$dist/$name.tar.gz
tar czf $archive *
echo "::set-output name=archive::$archive"
fi
- name: Publish Archive
uses: softprops/action-gh-release@v0.1.5
if: ${{ startsWith(github.ref, 'refs/tags/') }}
with:
draft: false
files: ${{ steps.package.outputs.archive }}
prerelease: ${{ steps.check-tag.outputs.rc == 'true' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

14
.gitignore vendored Normal file
View file

@ -0,0 +1,14 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/
result
.direnv/
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
pasta_data/*

2444
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,23 +1,41 @@
[package] [package]
name="microbin" name = "karton"
version="0.2.0" version = "2.0.1"
edition="2021" edition = "2021"
authors = ["Jade <jade@schrottkatze.de>", "Daniel Szabo <daniel.szabo99@outlook.com>"]
license = "BSD-3-Clause"
description = "Simple, performant, configurable, entirely self-contained Pastebin and URL shortener."
readme = "README.md"
homepage = "https://gitlab.com/obsidianical/microbin"
repository = "https://gitlab.com/obsidianical/microbin"
keywords = ["pastebin", "karton", "microbin", "actix", "selfhosted"]
categories = ["pastebins"]
[dependencies] [dependencies]
actix-web="4" actix-web = "4"
actix-files="0.6.0" actix-files = "0.6.0"
serde={ version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.80" serde_json = "1.0.80"
askama="0.10" bytesize = { version = "1.1", features = ["serde"] }
askama-filters={ version = "0.1.3", features = ["chrono"] } askama = "0.10"
chrono="0.4.19" askama-filters = { version = "0.1.3", features = ["chrono"] }
rand="0.8.5" chrono = "0.4.19"
linkify="0.8.1" rand = "0.8.5"
clap={ version = "3.1.12", features = ["derive"] } linkify = "0.8.1"
clap = { version = "3.1.12", features = ["derive", "env"] }
actix-multipart = "0.4.0" actix-multipart = "0.4.0"
futures = "0.3" futures = "0.3"
sanitize-filename = "0.3.0" sanitize-filename = "0.3.0"
log = "0.4" log = "0.4"
env_logger = "0.9.0" env_logger = "0.9.0"
actix-web-httpauth = "0.6.0" actix-web-httpauth = "0.6.0"
lazy_static = "1.4.0" lazy_static = "1.4.0"
syntect = "5.0"
qrcode-generator = "4.1.6"
rust-embed = "6.4.2"
mime_guess = "2.0.4"
harsh = "0.2"
[profile.release]
lto = true
strip = true

View file

@ -1,26 +1,37 @@
# latest rust will be used to build the binary FROM docker.io/rust:latest as build
FROM rust:latest as builder
# the temporary directory where we build WORKDIR /app
WORKDIR /usr/src/microbin
# copy sources to /usr/src/microbin on the temporary container
COPY . . COPY . .
# run release build RUN \
RUN cargo build --release DEBIAN_FRONTEND=noninteractive \
apt-get update &&\
apt-get -y install ca-certificates tzdata &&\
CARGO_NET_GIT_FETCH_WITH_CLI=true \
cargo build --release
# create final container using slim version of debian # https://hub.docker.com/r/bitnami/minideb
FROM debian:buster-slim FROM docker.io/bitnami/minideb:latest
# microbin will be in /usr/local/bin/microbin/ # microbin will be in /app
WORKDIR /usr/local/bin WORKDIR /app
# copy built exacutable # copy time zone info
COPY --from=builder /usr/src/microbin/target/release/microbin /usr/local/bin/microbin COPY --from=build \
/usr/share/zoneinfo \
/usr/share/zoneinfo
# copy /static folder containing the stylesheets COPY --from=build \
COPY --from=builder /usr/src/microbin/static /usr/local/bin/static /etc/ssl/certs/ca-certificates.crt \
/etc/ssl/certs/ca-certificates.crt
# run the binary # copy built executable
CMD ["microbin"] COPY --from=build \
/app/target/release/karton \
/usr/bin/karton
# Expose webport used for the webserver to the docker runtime
EXPOSE 8080
ENTRYPOINT ["karton"]

View file

@ -1,66 +0,0 @@
# MicroBin
![Screenshot](git/index.png)
MicroBin is a super tiny and simple self hosted pastebin app written in Rust. The executable is around 6MB and it uses 2MB memory (plus your pastas, because they are all stored in the memory at the moment).
### Features
- Is very small
- File uploads
- Raw pasta content (/raw/[animals])
- URL shortening and redirection
- Automatic dark mode (follows system preferences)
- Very simple database (json + files) for portability and easy backups
- Animal names instead of random numbers for pasta identifiers (64 animals)
- Automatically expiring pastas
- Never expiring pastas
- Listing and manually removing pastas (/pastalist)
- Very little CSS and absolutely no JS (see [water.css](https://github.com/kognise/water.css))
### Installation
Simply clone the repository, build it with `cargo build --release` and run the `microbin` executable in the created `target/release/` directory. It will start on port 8080. You can change the port with `-p` or `--port` CL arguments. For other arguments see [the Wiki](https://github.com/szabodanika/microbin/wiki).
To install it as a service on your Linux machine, create a file called `/etc/systemd/system/microbin.service`, paste this into it with the `[username]` and `[path to installation directory]` replaced with the actual values.
```
[Unit]
Description=MicroBin
After=network.target
[Service]
Type=simple
Restart=always
User=[username]
RootDirectory=/
WorkingDirectory=[path to installation directory]
ExecStart=[path to installation directory]/target/release/microbin
[Install]
WantedBy=multi-user.target
```
Then start the service with `systemctl start microbin` and enable it on boot with `systemctl enable microbin`.
### Create Pasta with cURL
Simple text Pasta: `curl -d "expiration=10min&content=This is a test pasta" -X POST https://microbin.myserver.com/create`
File contents: `curl -d "expiration=10min&content=$( < mypastafile.txt )" -X POST https://microbin.myserver.com/create`
Available expiration options:
`1min`, `10min`, `1hour`, `24hour`, `1week`, `never`
Use cURL to read the pasta: `curl https://microbin.myserver.com/rawpasta/fish-pony-crow`,
or to download the pasta: `curl https://microbin.myserver.com/rawpasta/fish-pony-crow > output.txt` (use /file instead of /rawpasta to download attached file).
### Needed improvements
- ~~Persisting pastas on disk (currently they are lost on restart)~~ (added on 2 May 2022)
- ~~Configuration with command line arguments (ports, enable-disable pasta list, footer, etc)~~ (added on 7 May 2022)
- ~~File uploads~~ (added on 2 May 2022)
- ~~URL shortening~~ (added on 23 April 2022)
- Removing pasta after N reads
- CLI tool (beyond wget)
- Better instructions and documentation - on GitHub and built in

76
README.md Normal file
View file

@ -0,0 +1,76 @@
# Karton
A small, rusty pastebin with URL shortener functionality.
The github repository is a mirror of [this gitlab repository](https://gitlab.com/obsidianical/microbin).
This is a fork of [MicroBin](https://github.com/szabodanika/microbin).
## Features
- Animal names (by default) or custom namefiles instead of just hashes (though hashes are an option too!)
- File and image uploads
- raw text serving
- URL shortening
- QR codes
- Listing and removing pastas (though currently everyone can do that)
- Expiration times
- Editable pastas
- Syntax highlighting
- Styling via [water.css](https://github.com/kognise/water.css)
- Customizable endpoints
## Installation guide
Karton is available on [Docker hub](https://hub.docker.com/r/schrottkatze/karton), [crates.io](https://crates.io/crates/karton) and using the nix flake.
The only "officially supported" (I will actively debug and search for the problem) method is the last one using nix flakes.
### Installation via the nix flake
Add the repository to your inputs.
```nix
karton.url = "git+https://gitlab.com/obsidianical/microbin.git";
```
```nix
# microbin.nix
{ inputs, config, pkgs, ... }:
{
environment.systemPackages = [ inputs.karton.defaultPackage."x86_64-linux" ];
systemd.services.karton = {
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
# set environment variables to configure karton
KARTON_HASH_IDS = "";
KARTON_EDITABLE = "";
KARTON_PRIVATE = "";
KARTON_HIGHLIGHTSYNTAX = "";
# adjust this to your domain
KARTON_PUBLIC_PATH = "https://example.org";
KARTON_QR = "";
# configure endpoints to be shorter
KARTON_URL_EP = "u";
KARTON_RAW_EP = "r";
KARTON_PASTA_EP = "p";
};
script = "${inputs.karton.defaultPackage."x86_64-linux"}/bin/karton";
# register a simple systemd service
serviceConfig = {
Type = "simple";
RootDirectory="/";
WorkingDirectory = "/karton";
};
};
}
```
## Contact
This fork of MicroBin was created by [Schrottkatze](https://schrottkatze.de).
Join [the matrix room](https://matrix.to/#/#s10e-microbin:matrix.org) to chat!
Contact me via e-mail at [contact@schrottkatze.de](mailto:contact@schrottkatze.de).

54
TODO.md Normal file
View file

@ -0,0 +1,54 @@
# TODO lists
these are just rough guides tho
## v2.1
- [ ] customizable endpoints
- [ ] create
- [ ] edit
- [ ] info
- [ ] get pastas
- [ ] remove
- [ ] improve remove endpoint
- [ ] disable it
- [ ] client library
- [ ] request .well-known data
- [ ] support most endpoints
- [ ] karton cli
## v3.0
- [ ] internal rewrite & docs
- [ ] design new frontend
- [ ] switch to yew
- [ ] using client lib
- [ ] theme and general config files
- [ ] unified theme format
- [ ] no env configs anymore if possible
- [ ] proper dbs
- [ ] sqlite
- [ ] postgres
- [ ] apis/endpoints
- [ ] IDs, name IDs AND user/pastaname
- [ ] root (and admin) user for root level pastas
- [ ] status/instance health admin dashboard and ap
- [ ] storage
- [ ] db status
- [ ] how up to date
- [ ] stats (users etc)
- [ ] errors
- [ ] loading speeds, performance monitor?
- [ ] memory use
- [ ] auth
- [ ] general auth
- [ ] oidc
- [ ] permssion system & api keys
- [ ] only allow some other users to open pasta
- [ ] access control and editing
- [ ] password protected pastas too
- [ ] features for pastas
- [ ] pw protection
- [ ] better editor
- [ ] markdown pastas
- [ ] optional, opt-in commenting

7
default.nix Normal file
View file

@ -0,0 +1,7 @@
(import (
fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz";
sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; }
) {
src = ./.;
}).defaultNix

77
flake.lock Normal file
View file

@ -0,0 +1,77 @@
{
"nodes": {
"naersk": {
"inputs": {
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1671096816,
"narHash": "sha256-ezQCsNgmpUHdZANDCILm3RvtO1xH8uujk/+EqNvzIOg=",
"owner": "nix-community",
"repo": "naersk",
"rev": "d998160d6a076cfe8f9741e56aeec7e267e3e114",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "master",
"repo": "naersk",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1677852945,
"narHash": "sha256-liiVJjkBTuBTAkRW3hrI8MbPD2ImYzwUpa7kvteiKhM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "f5ffd5787786dde3a8bf648c7a1b5f78c4e01abb",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1677852945,
"narHash": "sha256-liiVJjkBTuBTAkRW3hrI8MbPD2ImYzwUpa7kvteiKhM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "f5ffd5787786dde3a8bf648c7a1b5f78c4e01abb",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"naersk": "naersk",
"nixpkgs": "nixpkgs_2",
"utils": "utils"
}
},
"utils": {
"locked": {
"lastModified": 1676283394,
"narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

21
flake.nix Normal file
View file

@ -0,0 +1,21 @@
{
inputs = {
naersk.url = "github:nix-community/naersk/master";
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, utils, naersk }:
utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
naersk-lib = pkgs.callPackage naersk { };
in
{
defaultPackage = naersk-lib.buildPackage ./.;
devShell = with pkgs; mkShell {
buildInputs = [ cargo rustc rustfmt pre-commit rustPackages.clippy cargo-watch podman ];
RUST_SRC_PATH = rustPlatform.rustLibSrc;
};
});
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 480 KiB

7
shell.nix Normal file
View file

@ -0,0 +1,7 @@
(import (
fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz";
sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; }
) {
src = ./.;
}).shellNix

View file

@ -1,48 +0,0 @@
const ANIMAL_NAMES: &[&str] = &[
"ant", "eel", "mole", "sloth", "ape", "emu", "monkey", "snail", "bat", "falcon", "mouse",
"snake", "bear", "fish", "otter", "spider", "bee", "fly", "parrot", "squid", "bird", "fox",
"panda", "swan", "bison", "frog", "pig", "tiger", "camel", "gecko", "pigeon", "toad", "cat",
"goat", "pony", "turkey", "cobra", "goose", "pug", "turtle", "crow", "hawk", "rabbit", "viper",
"deer", "horse", "rat", "wasp", "dog", "jaguar", "raven", "whale", "dove", "koala", "seal",
"wolf", "duck", "lion", "shark", "worm", "eagle", "lizard", "sheep", "zebra",
];
pub fn to_animal_names(mut number: u64) -> String {
let mut result: Vec<&str> = Vec::new();
if number == 0 {
return ANIMAL_NAMES[0].parse().unwrap();
}
// max 4 animals so 6 * 6 = 64 bits
let mut power = 6;
loop {
let digit = number / ANIMAL_NAMES.len().pow(power) as u64;
if !(result.is_empty() && digit == 0) {
result.push(ANIMAL_NAMES[digit as usize]);
}
number -= digit * ANIMAL_NAMES.len().pow(power) as u64;
if power > 0 {
power -= 1;
} else if power <= 0 || number == 0 {
break;
}
}
result.join("-")
}
pub fn to_u64(animal_names: &str) -> u64 {
let mut result: u64 = 0;
let animals: Vec<&str> = animal_names.split("-").collect();
let mut pow = animals.len();
for i in 0..animals.len() {
pow -= 1;
result += (ANIMAL_NAMES.iter().position(|&r| r == animals[i]).unwrap()
* ANIMAL_NAMES.len().pow(pow as u32)) as u64;
}
result
}

166
src/args.rs Normal file
View file

@ -0,0 +1,166 @@
use clap::Parser;
use lazy_static::lazy_static;
use std::convert::Infallible;
use std::fmt;
use std::net::IpAddr;
use std::path::PathBuf;
use std::str::FromStr;
lazy_static! {
pub static ref ARGS: Args = Args::parse();
}
#[derive(Parser, Debug, Clone)]
#[clap(author, version, about, long_about = None)]
pub struct Args {
/// The username for basic HTTP auth.
/// If unset, HTTP authentication stays disabled.
///
/// WARNING: people opening pastas will have to authenticate too.
#[clap(long, env = "KARTON_AUTH_USERNAME")]
pub auth_username: Option<String>,
/// Set a password for HTTP authentication.
/// If unset, HTTP authentication will not require a password.
/// If `auth_username` is unset, this option will not have any effect.
#[clap(long, env = "KARTON_AUTH_PASSWORD")]
pub auth_password: Option<String>,
/// Enable the option to make pastas editable.
#[clap(long, env = "KARTON_EDITABLE")]
pub editable: bool,
/// The text displayed in the browser navigation bar.
#[clap(long, env = "KARTON_TITLE", default_value = " Karton")]
pub title: String,
/// The web interfaces' footer text.
#[clap(long, env = "KARTON_FOOTER_TEXT")]
pub footer_text: Option<String>,
/// Hide the footer of the web interface.
#[clap(long, env = "KARTON_HIDE_FOOTER")]
pub hide_footer: bool,
/// Hide the header of the web interface.
#[clap(long, env = "KARTON_HIDE_HEADER")]
pub hide_header: bool,
/// Hide the logo in the header.
#[clap(long, env = "KARTON_HIDE_LOGO")]
pub hide_logo: bool,
/// Disable the listing page.
#[clap(long, env = "KARTON_NO_LISTING")]
pub no_listing: bool,
/// Enable syntax highlighting in pastas.
#[clap(long, env = "KARTON_HIGHLIGHTSYNTAX")]
pub highlightsyntax: bool,
/// The port to which to bind the server.
#[clap(short, long, env = "KARTON_PORT", default_value_t = 8080)]
pub port: u16,
/// The IP adress to bind the server to.
#[clap(short, long, env="KARTON_BIND", default_value_t = IpAddr::from([0, 0, 0, 0]))]
pub bind: IpAddr,
/// Enable the option to create private pastas.
#[clap(long, env = "KARTON_PRIVATE")]
pub private: bool,
/// Disables most css, apart form some inline styles.
#[clap(long, env = "KARTON_PURE_HTML")]
pub pure_html: bool,
/// The servers public path, making it possible to run Karton behind a reverse proxy subpath.
#[clap(long, env="KARTON_PUBLIC_PATH", default_value_t = PublicUrl(String::from("")))]
pub public_path: PublicUrl,
/// Enable creation of QR codes of pastas. Requires `public_path` to be set.
#[clap(long, env = "KARTON_QR")]
pub qr: bool,
/// Disable adding/removing/editing pastas.
#[clap(long, env = "KARTON_READONLY")]
pub readonly: bool,
/// The amount of worker threads that the server is allowed to have.
#[clap(short, long, env = "KARTON_THREADS", default_value_t = 1)]
pub threads: u8,
/// Sets a time value for the garbage collector. Pastas that aren't accessed for the given
/// amount of days will be deleted. Set to 0 to disable garbage collection.
#[clap(short, long, env = "KARTON_GC_DAYS", default_value_t = 90)]
pub gc_days: u16,
/// Enable the option to delete after a given amount of reads.
#[clap(long, env = "KARTON_ENABLE_BURN_AFTER")]
pub enable_burn_after: bool,
/// The default amount of reads for the self-delete mechanism.
#[clap(short, long, env = "KARTON_DEFAULT_BURN_AFTER", default_value_t = 0)]
pub default_burn_after: u16,
/// Changes the UIs maximum width from 720 pixels to 1080.
#[clap(long, env = "KARTON_WIDE")]
pub wide: bool,
/// Disable "Never" expiry setting.
#[clap(long, env = "KARTON_NO_ETERNAL_PASTA")]
pub no_eternal_pasta: bool,
/// Set the default expiry time value.
#[clap(long, env = "KARTON_DEFAULT_EXPIRY", default_value = "24hour")]
pub default_expiry: String,
/// Disable file uploading.
#[clap(short, long, env = "KARTON_NO_FILE_UPLOAD")]
pub no_file_upload: bool,
// TODO: replace with simple path.
/// Replace built-in CSS file with a CSS file provided by the linked URL.
#[clap(long, env = "KARTON_CUSTOM_CSS")]
pub custom_css: Option<String>,
/// Replace built-in animal names file with custom names file for pasta links.
/// The file must be newline seperated.
#[clap(long, env = "KARTON_CUSTOM_NAMES")]
pub custom_names: Option<PathBuf>,
/// Enable the use of Hash IDs for shorter URLs instead of animal names.
#[clap(long, env = "KARTON_HASH_IDS")]
pub hash_ids: bool,
/// Endpoint for /url/
#[clap(long, env = "KARTON_URL_EP", default_value = "url" )]
pub url_endpoint: String,
/// Endpoint for /pasta/
#[clap(long, env = "KARTON_PASTA_EP", default_value = "pasta" )]
pub pasta_endpoint: String,
/// Endpoint for /raw/
#[clap(long, env = "KARTON_RAW_EP", default_value = "raw" )]
pub raw_endpoint: String,
}
#[derive(Debug, Clone)]
pub struct PublicUrl(pub String);
impl fmt::Display for PublicUrl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for PublicUrl {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let uri = s.strip_suffix('/').unwrap_or(s).to_owned();
Ok(PublicUrl(uri))
}
}

214
src/endpoints/create.rs Normal file
View file

@ -0,0 +1,214 @@
use crate::dbio::save_to_file;
use crate::pasta::PastaFile;
use crate::util::hashids::to_hashids;
use crate::util::misc::is_valid_url;
use crate::util::pasta_id_converter::CONVERTER;
use crate::{AppState, Pasta, ARGS};
use actix_multipart::Multipart;
use actix_web::http::StatusCode;
use actix_web::web::{BytesMut, BufMut};
use actix_web::{get, web, Error, HttpResponse, Responder};
use askama::Template;
use bytesize::ByteSize;
use futures::TryStreamExt;
use log::warn;
use rand::Rng;
use std::io::Write;
use std::time::{SystemTime, UNIX_EPOCH};
use super::errors::ErrorTemplate;
#[derive(Template)]
#[template(path = "index.html")]
struct IndexTemplate<'a> {
args: &'a ARGS,
}
#[get("/")]
pub async fn index() -> impl Responder {
HttpResponse::Ok()
.content_type("text/html")
.body(IndexTemplate { args: &ARGS }.render().unwrap())
}
/// Pasta creation endpoint.
pub async fn create(
data: web::Data<AppState>,
mut payload: Multipart,
) -> Result<HttpResponse, Error> {
if ARGS.readonly {
return Ok(HttpResponse::Found()
.append_header(("Location", format!("{}/", ARGS.public_path)))
.finish());
}
let mut pastas = data.pastas.lock().await;
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(_) => {
log::error!("SystemTime before UNIX EPOCH!");
0
}
} as i64;
let mut new_pasta = Pasta {
id: rand::thread_rng().gen::<u16>() as u64,
content: String::from("No Text Content"),
file: None,
extension: String::from(""),
private: false,
editable: false,
created: timenow,
read_count: 0,
burn_after_reads: 0,
last_read: timenow,
pasta_type: String::from(""),
expiration: 0,
};
while let Some(mut field) = payload.try_next().await? {
match field.name() {
"editable" => {
new_pasta.editable = true;
}
"private" => {
new_pasta.private = true;
}
"expiration" => {
while let Some(chunk) = field.try_next().await? {
new_pasta.expiration = match std::str::from_utf8(&chunk).unwrap() {
// TODO: customizable times
"1min" => timenow + 60,
"10min" => timenow + 60 * 10,
"1hour" => timenow + 60 * 60,
"24hour" => timenow + 60 * 60 * 24,
"3days" => timenow + 60 * 60 * 24 * 3,
"1week" => timenow + 60 * 60 * 24 * 7,
"never" => {
if ARGS.no_eternal_pasta {
timenow + 60 * 60 * 24 * 7
} else {
0
}
}
_ => {
log::error!("{}", "Unexpected expiration time!");
timenow + 60 * 60 * 24 * 7
}
};
}
}
"burn_after" => {
while let Some(chunk) = field.try_next().await? {
new_pasta.burn_after_reads = match std::str::from_utf8(&chunk).unwrap() {
// TODO: also make customizable
// maybe options in config files, with defaults
// give an extra read because the user will be redirected to the pasta page automatically
"1" => 2,
"10" => 10,
"100" => 100,
"1000" => 1000,
"10000" => 10000,
"0" => 0,
_ => {
log::error!("{}", "Unexpected burn after value!");
0
}
};
}
}
"content" => {
let mut content = BytesMut::new();
while let Some(chunk) = field.try_next().await? {
content.put(chunk);
}
if !content.is_empty() {
new_pasta.content = match String::from_utf8(content.to_vec()) {
Ok(v) => v,
Err(_) => return Ok(HttpResponse::BadRequest()
.content_type("text/html")
.body(ErrorTemplate {
status_code: StatusCode::BAD_REQUEST,
args: &ARGS
}.render().unwrap())),
};
new_pasta.pasta_type = if is_valid_url(new_pasta.content.as_str()) {
String::from("url")
} else {
String::from("text")
};
}
}
"syntax-highlight" => {
while let Some(chunk) = field.try_next().await? {
new_pasta.extension = std::str::from_utf8(&chunk).unwrap().to_string();
}
}
"file" => {
if ARGS.no_file_upload {
continue;
}
let path = field.content_disposition().get_filename();
let path = match path {
Some("") => continue,
Some(p) => p,
None => continue,
};
let mut file = match PastaFile::from_unsanitized(path) {
Ok(f) => f,
Err(e) => {
warn!("Unsafe file name: {e:?}");
continue;
}
};
std::fs::create_dir_all(format!(
"./pasta_data/public/{}",
&new_pasta.id_as_animals()
))
.unwrap();
let filepath = format!(
"./pasta_data/public/{}/{}",
&new_pasta.id_as_animals(),
&file.name()
);
let mut f = web::block(|| std::fs::File::create(filepath)).await??;
let mut size = 0;
while let Some(chunk) = field.try_next().await? {
size += chunk.len();
f = web::block(move || f.write_all(&chunk).map(|_| f)).await??;
}
file.size = ByteSize::b(size as u64);
new_pasta.file = Some(file);
new_pasta.pasta_type = String::from("text");
}
field => {
log::error!("Unexpected multipart field: {}", field);
}
}
}
let id = new_pasta.id;
pastas.push(new_pasta);
save_to_file(&pastas);
let slug = if ARGS.hash_ids {
to_hashids(id)
} else {
CONVERTER.to_names(id)
};
Ok(HttpResponse::Found()
.append_header(("Location", format!("{}/{}/{}", ARGS.public_path, ARGS.pasta_endpoint, slug)))
.finish())
}

110
src/endpoints/edit.rs Normal file
View file

@ -0,0 +1,110 @@
use crate::args::Args;
use crate::dbio::save_to_file;
use crate::endpoints::errors::ErrorTemplate;
use crate::util::hashids::to_u64 as hashid_to_u64;
use crate::util::misc::remove_expired;
use crate::util::pasta_id_converter::CONVERTER;
use crate::{AppState, Pasta, ARGS};
use actix_multipart::Multipart;
use actix_web::http::StatusCode;
use actix_web::{get, post, web, Error, HttpResponse};
use askama::Template;
use futures::TryStreamExt;
#[derive(Template)]
#[template(path = "edit.html", escape = "none")]
struct EditTemplate<'a> {
pasta: &'a Pasta,
args: &'a Args,
}
#[get("/edit/{id}")]
pub async fn get_edit(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
let mut pastas = data.pastas.lock().await;
let id = if ARGS.hash_ids {
hashid_to_u64(&id).unwrap_or(0)
} else {
CONVERTER.to_u64(&id.into_inner()).unwrap_or(0)
};
remove_expired(&mut pastas);
for pasta in pastas.iter() {
if pasta.id == id {
if !pasta.editable {
return HttpResponse::Found()
.append_header(("Location", format!("{}/", ARGS.public_path)))
.finish();
}
return HttpResponse::Ok()
.content_type("text/html")
.body(EditTemplate { pasta, args: &ARGS }.render().unwrap());
}
}
HttpResponse::NotFound()
.content_type("text/html")
.body(ErrorTemplate {
status_code: StatusCode::NOT_FOUND,
args: &ARGS
}.render().unwrap())
}
#[post("/edit/{id}")]
pub async fn post_edit(
data: web::Data<AppState>,
id: web::Path<String>,
mut payload: Multipart,
) -> Result<HttpResponse, Error> {
if ARGS.readonly {
return Ok(HttpResponse::Found()
.append_header(("Location", format!("{}/", ARGS.public_path)))
.finish());
}
let id = if ARGS.hash_ids {
hashid_to_u64(&id).unwrap_or(0)
} else {
CONVERTER.to_u64(&id.into_inner()).unwrap_or(0)
};
let mut pastas = data.pastas.lock().await;
remove_expired(&mut pastas);
let mut new_content = String::from("");
while let Some(mut field) = payload.try_next().await? {
if field.name() == "content" {
while let Some(chunk) = field.try_next().await? {
new_content = std::str::from_utf8(&chunk).unwrap().to_string();
}
}
}
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == id {
if pasta.editable {
pastas[i].content.replace_range(.., &new_content);
save_to_file(&pastas);
return Ok(HttpResponse::Found()
.append_header((
"Location",
format!("{}/pasta/{}", ARGS.public_path, pastas[i].id_as_animals()),
))
.finish());
} else {
break;
}
}
}
Ok(HttpResponse::NotFound()
.content_type("text/html")
.body(ErrorTemplate {
status_code: StatusCode::NOT_FOUND,
args: &ARGS
}.render().unwrap()))
}

20
src/endpoints/errors.rs Normal file
View file

@ -0,0 +1,20 @@
use actix_web::{Error, HttpResponse, http::StatusCode};
use askama::Template;
use crate::args::{Args, ARGS};
#[derive(Template)]
#[template(path = "error.html")]
pub struct ErrorTemplate<'a> {
pub status_code: StatusCode,
pub args: &'a Args,
}
pub async fn not_found() -> Result<HttpResponse, Error> {
Ok(HttpResponse::NotFound()
.content_type("text/html")
.body(ErrorTemplate {
status_code: StatusCode::NOT_FOUND,
args: &ARGS
}.render().unwrap()))
}

47
src/endpoints/info.rs Normal file
View file

@ -0,0 +1,47 @@
use crate::args::{Args, ARGS};
use crate::pasta::Pasta;
use crate::AppState;
use actix_web::{get, web, HttpResponse};
use askama::Template;
#[derive(Template)]
#[template(path = "info.html")]
struct Info<'a> {
args: &'a Args,
pastas: &'a Vec<Pasta>,
status: &'a str,
version_string: &'a str,
message: &'a str,
}
/// Endpoint to get information about the instance.
#[get("/info")]
pub async fn info(data: web::Data<AppState>) -> HttpResponse {
// get access to the pasta collection
let pastas = data.pastas.lock().await;
// TODO: status report more sophisticated
// maybe:
// - detect weird/invalid configurations?
// - detect server storage issues
// - detect performance problems?
let mut status = "OK";
let mut message = "";
if ARGS.public_path.to_string() == "" {
status = "WARNING";
message = "Warning: No public URL set with --public-path parameter. QR code and URL Copying functions have been disabled"
}
HttpResponse::Ok().content_type("text/html").body(
Info {
args: &ARGS,
pastas: &pastas,
status,
version_string: env!("CARGO_PKG_VERSION"),
message
}
.render()
.unwrap(),
)
}

213
src/endpoints/pasta.rs Normal file
View file

@ -0,0 +1,213 @@
use crate::args::{Args, ARGS};
use crate::dbio::save_to_file;
use crate::endpoints::errors::ErrorTemplate;
use crate::pasta::Pasta;
use crate::util::hashids::to_u64 as hashid_to_u64;
use crate::util::misc::remove_expired;
use crate::AppState;
use crate::util::pasta_id_converter::CONVERTER;
use actix_web::http::StatusCode;
use actix_web::{web, HttpResponse};
use askama::Template;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Template)]
#[template(path = "pasta.html", escape = "none")]
struct PastaTemplate<'a> {
pasta: &'a Pasta,
args: &'a Args,
}
/// Endpoint to view a pasta.
pub async fn getpasta(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
// get access to the pasta collection
let mut pastas = data.pastas.lock().await;
let id = if ARGS.hash_ids {
hashid_to_u64(&id).unwrap_or(0)
} else {
CONVERTER.to_u64(&id.into_inner()).unwrap_or(0)
};
// remove expired pastas (including this one if needed)
remove_expired(&mut pastas);
// find the index of the pasta in the collection based on u64 id
let mut index: usize = 0;
let mut found: bool = false;
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == id {
index = i;
found = true;
break;
}
}
if found {
// increment read count
pastas[index].read_count += 1;
// save the updated read count
save_to_file(&pastas);
// serve pasta in template
let response = HttpResponse::Ok().content_type("text/html").body(
PastaTemplate {
pasta: &pastas[index],
args: &ARGS,
}
.render()
.unwrap(),
);
// get current unix time in seconds
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(_) => {
log::error!("SystemTime before UNIX EPOCH!");
0
}
} as i64;
// update last read time
pastas[index].last_read = timenow;
return response;
}
// otherwise
// send pasta not found error
HttpResponse::NotFound()
.content_type("text/html")
.body(ErrorTemplate {
status_code: StatusCode::NOT_FOUND,
args: &ARGS
}.render().unwrap())
}
/// Endpoint for redirection.
pub async fn redirecturl(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
// get access to the pasta collection
let mut pastas = data.pastas.lock().await;
let id = if ARGS.hash_ids {
hashid_to_u64(&id).unwrap_or(0)
} else {
CONVERTER.to_u64(&id.into_inner()).unwrap_or(0)
};
// remove expired pastas (including this one if needed)
remove_expired(&mut pastas);
// find the index of the pasta in the collection based on u64 id
let mut index: usize = 0;
let mut found: bool = false;
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == id {
index = i;
found = true;
break;
}
}
if found {
// increment read count
pastas[index].read_count += 1;
// save the updated read count
save_to_file(&pastas);
// send redirect if it's a url pasta
if pastas[index].pasta_type == "url" {
let response = HttpResponse::Found()
.append_header(("Location", String::from(&pastas[index].content)))
.finish();
// get current unix time in seconds
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(_) => {
log::error!("SystemTime before UNIX EPOCH!");
0
}
} as i64;
// update last read time
pastas[index].last_read = timenow;
return response;
// send error if we're trying to open a non-url pasta as a redirect
} else {
HttpResponse::NotFound()
.content_type("text/html")
.body(ErrorTemplate {
status_code: StatusCode::NOT_FOUND,
args: &ARGS
}.render().unwrap());
}
}
// otherwise
// send pasta not found error
HttpResponse::NotFound()
.content_type("text/html")
.body(ErrorTemplate {
status_code: StatusCode::NOT_FOUND,
args: &ARGS
}.render().unwrap())
}
/// Endpoint to request pasta as raw file.
pub async fn getrawpasta(data: web::Data<AppState>, id: web::Path<String>) -> String {
// get access to the pasta collection
let mut pastas = data.pastas.lock().await;
let id = if ARGS.hash_ids {
hashid_to_u64(&id).unwrap_or(0)
} else {
CONVERTER.to_u64(&id.into_inner()).unwrap_or(0)
};
// remove expired pastas (including this one if needed)
remove_expired(&mut pastas);
// find the index of the pasta in the collection based on u64 id
let mut index: usize = 0;
let mut found: bool = false;
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == id {
index = i;
found = true;
break;
}
}
if found {
// increment read count
pastas[index].read_count += 1;
// get current unix time in seconds
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(_) => {
log::error!("SystemTime before UNIX EPOCH!");
0
}
} as i64;
// update last read time
pastas[index].last_read = timenow;
// save the updated read count
save_to_file(&pastas);
// send raw content of pasta
return pastas[index].content.to_owned();
}
// otherwise
// send pasta not found error as raw text
String::from("Pasta not found! :-(")
}

View file

@ -0,0 +1,37 @@
use actix_web::{get, web, HttpResponse};
use askama::Template;
use crate::args::{Args, ARGS};
use crate::pasta::Pasta;
use crate::util::misc::remove_expired;
use crate::AppState;
#[derive(Template)]
#[template(path = "pastalist.html")]
struct PastaListTemplate<'a> {
pastas: &'a Vec<Pasta>,
args: &'a Args,
}
/// The endpoint to view all currently registered pastas.
#[get("/pastalist")]
pub async fn list(data: web::Data<AppState>) -> HttpResponse {
if ARGS.no_listing {
return HttpResponse::Found()
.append_header(("Location", format!("{}/", ARGS.public_path)))
.finish();
}
let mut pastas = data.pastas.lock().await;
remove_expired(&mut pastas);
HttpResponse::Ok().content_type("text/html").body(
PastaListTemplate {
pastas: &pastas,
args: &ARGS,
}
.render()
.unwrap(),
)
}

73
src/endpoints/qr.rs Normal file
View file

@ -0,0 +1,73 @@
use crate::args::{Args, ARGS};
use crate::endpoints::errors::ErrorTemplate;
use crate::pasta::Pasta;
use crate::util::hashids::to_u64 as hashid_to_u64;
use crate::util::misc::{self, remove_expired};
use crate::AppState;
use crate::util::pasta_id_converter::CONVERTER;
use actix_web::http::StatusCode;
use actix_web::{get, web, HttpResponse};
use askama::Template;
#[derive(Template)]
#[template(path = "qr.html", escape = "none")]
struct QRTemplate<'a> {
qr: &'a String,
pasta: &'a Pasta,
args: &'a Args,
}
/// Endpoint to open a QR code to a pasta.
#[get("/qr/{id}")]
pub async fn getqr(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
// get access to the pasta collection
let mut pastas = data.pastas.lock().await;
let u64_id = if ARGS.hash_ids {
hashid_to_u64(&id).unwrap_or(0)
} else {
CONVERTER.to_u64(&id).unwrap_or(0)
};
// remove expired pastas (including this one if needed)
remove_expired(&mut pastas);
// find the index of the pasta in the collection based on u64 id
let mut index: usize = 0;
let mut found: bool = false;
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == u64_id {
index = i;
found = true;
break;
}
}
if found {
// generate the QR code as an SVG - if its a file or text pastas, this will point to the /pasta endpoint, otherwise to the /url endpoint, essentially directly taking the user to the url stored in the pasta
let svg: String = match pastas[index].pasta_type.as_str() {
"url" => misc::string_to_qr_svg(format!("{}/url/{}", &ARGS.public_path, &id).as_str()),
_ => misc::string_to_qr_svg(format!("{}/pasta/{}", &ARGS.public_path, &id).as_str()),
};
// serve qr code in template
return HttpResponse::Ok().content_type("text/html").body(
QRTemplate {
qr: &svg,
pasta: &pastas[index],
args: &ARGS,
}
.render()
.unwrap(),
);
}
// otherwise
// send pasta not found error
HttpResponse::NotFound()
.content_type("text/html")
.body(ErrorTemplate {
status_code: StatusCode::NOT_FOUND,
args: &ARGS
}.render().unwrap())
}

68
src/endpoints/remove.rs Normal file
View file

@ -0,0 +1,68 @@
use actix_web::http::StatusCode;
use actix_web::{get, web, HttpResponse};
use crate::args::ARGS;
use crate::endpoints::errors::ErrorTemplate;
use crate::pasta::PastaFile;
use crate::util::hashids::to_u64 as hashid_to_u64;
use crate::util::misc::remove_expired;
use crate::AppState;
use crate::util::pasta_id_converter::CONVERTER;
use askama::Template;
use std::fs;
/// Endpoint to remove a pasta.
#[get("/remove/{id}")]
pub async fn remove(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
if ARGS.readonly {
return HttpResponse::Found()
.append_header(("Location", format!("{}/", ARGS.public_path)))
.finish();
}
let mut pastas = data.pastas.lock().await;
let id = if ARGS.hash_ids {
hashid_to_u64(&id).unwrap_or(0)
} else {
CONVERTER.to_u64(&id.into_inner()).unwrap_or(0)
};
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == id {
// remove the file itself
if let Some(PastaFile { name, .. }) = &pasta.file {
if fs::remove_file(format!(
"./pasta_data/public/{}/{}",
pasta.id_as_animals(),
name
))
.is_err()
{
log::error!("Failed to delete file {}!", name)
}
// and remove the containing directory
if fs::remove_dir(format!("./pasta_data/public/{}/", pasta.id_as_animals()))
.is_err()
{
log::error!("Failed to delete directory {}!", name)
}
}
// remove it from in-memory pasta list
pastas.remove(i);
return HttpResponse::Found()
.append_header(("Location", format!("{}/pastalist", ARGS.public_path)))
.finish();
}
}
remove_expired(&mut pastas);
HttpResponse::NotFound()
.content_type("text/html")
.body(ErrorTemplate {
status_code: StatusCode::NOT_FOUND,
args: &ARGS
}.render().unwrap())
}

View file

@ -0,0 +1,21 @@
use actix_web::{web, HttpResponse, Responder};
use mime_guess::from_path;
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "templates/assets/"]
struct Asset;
fn handle_embedded_file(path: &str) -> HttpResponse {
match Asset::get(path) {
Some(content) => HttpResponse::Ok()
.content_type(from_path(path).first_or_octet_stream().as_ref())
.body(content.data.into_owned()),
None => HttpResponse::NotFound().body("404 Not Found"),
}
}
#[actix_web::get("/static/{_:.*}")]
async fn static_resources(path: web::Path<String>) -> impl Responder {
handle_embedded_file(path.as_str())
}

View file

@ -1,331 +1,51 @@
extern crate core; extern crate core;
use env_logger::Builder; use crate::args::ARGS;
use std::io::Write; use crate::endpoints::{
use std::sync::Mutex; create, edit, errors, info, pasta as pasta_endpoint, pastalist, qr, remove, static_resources,
use std::time::{SystemTime, UNIX_EPOCH}; };
use actix_files;
use actix_multipart::Multipart;
use actix_web::dev::ServiceRequest;
use actix_web::middleware::Condition;
use actix_web::{error, get, middleware, web, App, Error, HttpResponse, HttpServer, Responder};
use actix_web_httpauth::extractors::basic::BasicAuth;
use actix_web_httpauth::middleware::HttpAuthentication;
use askama::Template;
use chrono::Local;
use clap::Parser;
use futures::TryStreamExt as _;
use lazy_static::lazy_static;
use linkify::{LinkFinder, LinkKind};
use log::LevelFilter;
use rand::Rng;
use std::fs;
use crate::animalnumbers::{to_animal_names, to_u64};
use crate::dbio::save_to_file;
use crate::pasta::Pasta; use crate::pasta::Pasta;
use crate::util::dbio;
use actix_web::middleware::Condition;
use actix_web::{middleware, web, App, HttpServer};
use actix_web_httpauth::middleware::HttpAuthentication;
use chrono::Local;
use env_logger::Builder;
use futures::lock::Mutex;
use log::LevelFilter;
use std::fs;
use std::io::Write;
mod animalnumbers; pub mod args;
mod dbio; pub mod pasta;
mod pasta;
lazy_static! { pub mod util {
static ref ARGS: Args = Args::parse(); pub mod pasta_id_converter;
pub mod auth;
pub mod dbio;
pub mod hashids;
pub mod misc;
pub mod syntaxhighlighter;
} }
struct AppState { pub mod endpoints {
pastas: Mutex<Vec<Pasta>>, pub mod create;
pub mod edit;
pub mod errors;
pub mod info;
pub mod pasta;
pub mod pastalist;
pub mod qr;
pub mod remove;
pub mod static_resources;
} }
#[derive(Parser, Debug, Clone)] pub struct AppState {
#[clap(author, version, about, long_about = None)] pub pastas: Mutex<Vec<Pasta>>,
struct Args {
#[clap(short, long, default_value_t = 8080)]
port: u32,
#[clap(short, long, default_value_t = 1)]
threads: u8,
#[clap(long)]
hide_header: bool,
#[clap(long)]
hide_footer: bool,
#[clap(long)]
pure_html: bool,
#[clap(long)]
no_listing: bool,
#[clap(long)]
auth_username: Option<String>,
#[clap(long)]
auth_password: Option<String>,
}
async fn auth_validator(
req: ServiceRequest,
credentials: BasicAuth,
) -> Result<ServiceRequest, Error> {
// check if username matches
if credentials.user_id().as_ref() == ARGS.auth_username.as_ref().unwrap() {
return match ARGS.auth_password.as_ref() {
Some(cred_pass) => match credentials.password() {
None => Err(error::ErrorBadRequest("Invalid login details.")),
Some(arg_pass) => {
if arg_pass == cred_pass {
Ok(req)
} else {
Err(error::ErrorBadRequest("Invalid login details."))
}
}
},
None => Ok(req),
};
} else {
Err(error::ErrorBadRequest("Invalid login details."))
}
}
#[derive(Template)]
#[template(path = "index.html")]
struct IndexTemplate<'a> {
args: &'a Args,
}
#[derive(Template)]
#[template(path = "error.html")]
struct ErrorTemplate<'a> {
args: &'a Args,
}
#[derive(Template)]
#[template(path = "pasta.html")]
struct PastaTemplate<'a> {
pasta: &'a Pasta,
args: &'a Args,
}
#[derive(Template)]
#[template(path = "pastalist.html")]
struct PastaListTemplate<'a> {
pastas: &'a Vec<Pasta>,
args: &'a Args,
}
#[get("/")]
async fn index() -> impl Responder {
HttpResponse::Found()
.content_type("text/html")
.body(IndexTemplate { args: &ARGS }.render().unwrap())
}
async fn not_found() -> Result<HttpResponse, Error> {
Ok(HttpResponse::Found()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap()))
}
async fn create(data: web::Data<AppState>, mut payload: Multipart) -> Result<HttpResponse, Error> {
let mut pastas = data.pastas.lock().unwrap();
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(_) => panic!("SystemTime before UNIX EPOCH!"),
} as i64;
let mut new_pasta = Pasta {
id: rand::thread_rng().gen::<u16>() as u64,
content: String::from("No Text Content"),
file: String::from("no-file"),
created: timenow,
pasta_type: String::from(""),
expiration: 0,
};
while let Some(mut field) = payload.try_next().await? {
match field.name() {
"expiration" => {
while let Some(chunk) = field.try_next().await? {
new_pasta.expiration = match std::str::from_utf8(&chunk).unwrap() {
"1min" => timenow + 60,
"10min" => timenow + 60 * 10,
"1hour" => timenow + 60 * 60,
"24hour" => timenow + 60 * 60 * 24,
"1week" => timenow + 60 * 60 * 24 * 7,
"never" => 0,
_ => panic!("Unexpected expiration time!"),
};
}
continue;
}
"content" => {
while let Some(chunk) = field.try_next().await? {
new_pasta.content = std::str::from_utf8(&chunk).unwrap().to_string();
new_pasta.pasta_type = if is_valid_url(new_pasta.content.as_str()) {
String::from("url")
} else {
String::from("text")
};
}
continue;
}
"file" => {
let content_disposition = field.content_disposition();
let filename = match content_disposition.get_filename() {
Some("") => continue,
Some(filename) => filename.replace(' ', "_").to_string(),
None => continue,
};
std::fs::create_dir_all(format!("./pasta_data/{}", &new_pasta.id_as_animals()))
.unwrap();
let filepath = format!("./pasta_data/{}/{}", &new_pasta.id_as_animals(), &filename);
new_pasta.file = filename;
let mut f = web::block(|| std::fs::File::create(filepath)).await??;
while let Some(chunk) = field.try_next().await? {
f = web::block(move || f.write_all(&chunk).map(|_| f)).await??;
}
new_pasta.pasta_type = String::from("text");
}
_ => {}
}
}
let id = new_pasta.id;
pastas.push(new_pasta);
save_to_file(&pastas);
Ok(HttpResponse::Found()
.append_header(("Location", format!("/pasta/{}", to_animal_names(id))))
.finish())
}
#[get("/pasta/{id}")]
async fn getpasta(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
let mut pastas = data.pastas.lock().unwrap();
let id = to_u64(&*id.into_inner());
remove_expired(&mut pastas);
for pasta in pastas.iter() {
if pasta.id == id {
return HttpResponse::Found()
.content_type("text/html")
.body(PastaTemplate { pasta, args: &ARGS }.render().unwrap());
}
}
HttpResponse::Found()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}
#[get("/url/{id}")]
async fn redirecturl(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
let mut pastas = data.pastas.lock().unwrap();
let id = to_u64(&*id.into_inner());
remove_expired(&mut pastas);
for pasta in pastas.iter() {
if pasta.id == id {
if pasta.pasta_type == "url" {
return HttpResponse::Found()
.append_header(("Location", String::from(&pasta.content)))
.finish();
} else {
return HttpResponse::Found()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap());
}
}
}
HttpResponse::Found()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}
#[get("/raw/{id}")]
async fn getrawpasta(data: web::Data<AppState>, id: web::Path<String>) -> String {
let mut pastas = data.pastas.lock().unwrap();
let id = to_u64(&*id.into_inner());
remove_expired(&mut pastas);
for pasta in pastas.iter() {
if pasta.id == id {
return pasta.content.to_owned();
}
}
String::from("Pasta not found! :-(")
}
#[get("/remove/{id}")]
async fn remove(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
let mut pastas = data.pastas.lock().unwrap();
let id = to_u64(&*id.into_inner());
remove_expired(&mut pastas);
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == id {
pastas.remove(i);
return HttpResponse::Found()
.append_header(("Location", "/pastalist"))
.finish();
}
}
HttpResponse::Found()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}
#[get("/pastalist")]
async fn list(data: web::Data<AppState>) -> HttpResponse {
if ARGS.no_listing {
return HttpResponse::Found()
.append_header(("Location", "/"))
.finish();
}
let mut pastas = data.pastas.lock().unwrap();
remove_expired(&mut pastas);
HttpResponse::Found().content_type("text/html").body(
PastaListTemplate {
pastas: &pastas,
args: &ARGS,
}
.render()
.unwrap(),
)
} }
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
let args: Args = Args::parse();
Builder::new() Builder::new()
.format(|buf, record| { .format(|buf, record| {
writeln!( writeln!(
@ -340,15 +60,16 @@ async fn main() -> std::io::Result<()> {
.init(); .init();
log::info!( log::info!(
"MicroBin starting on http://127.0.0.1:{}", "MicroBin starting on http://{}:{}",
args.port.to_string() ARGS.bind.to_string(),
ARGS.port.to_string()
); );
match std::fs::create_dir_all("./pasta_data") { match fs::create_dir_all("./pasta_data/public") {
Ok(dir) => dir, Ok(dir) => dir,
Err(error) => { Err(error) => {
log::error!("Couldn't create data directory ./pasta_data: {:?}", error); log::error!("Couldn't create data directory ./pasta_data/public/: {error:?}");
panic!("Couldn't create data directory ./pasta_data: {:?}", error); panic!("Couldn't create data directory ./pasta_data/public/: {error:?}");
} }
}; };
@ -360,53 +81,40 @@ async fn main() -> std::io::Result<()> {
App::new() App::new()
.app_data(data.clone()) .app_data(data.clone())
.wrap(middleware::NormalizePath::trim()) .wrap(middleware::NormalizePath::trim())
.service(index) .service(create::index)
.service(getpasta) .service(info::info)
.service(redirecturl) .route(
.service(getrawpasta) &format!("/{}/{{id}}", ARGS.pasta_endpoint),
.service(actix_files::Files::new("/static", "./static")) web::get().to(pasta_endpoint::getpasta)
.service(actix_files::Files::new("/file", "./pasta_data")) )
.service(web::resource("/upload").route(web::post().to(create))) .route(
.default_service(web::route().to(not_found)) &format!("/{}/{{id}}", ARGS.raw_endpoint),
web::get().to(pasta_endpoint::getrawpasta)
)
.route(
&format!("/{}/{{id}}", ARGS.url_endpoint),
web::get().to(pasta_endpoint::redirecturl)
)
//.service(pasta_endpoint::getpasta)
//.service(pasta_endpoint::getrawpasta)
//.service(pasta_endpoint::redirecturl)
.service(edit::get_edit)
.service(edit::post_edit)
.service(static_resources::static_resources)
.service(qr::getqr)
.service(actix_files::Files::new("/file", "./pasta_data/public/"))
.service(web::resource("/upload").route(web::post().to(create::create)))
.default_service(web::route().to(errors::not_found))
.wrap(middleware::Logger::default()) .wrap(middleware::Logger::default())
.service(remove) .service(remove::remove)
.service(list) .service(pastalist::list)
.wrap(Condition::new( .wrap(Condition::new(
args.auth_username.is_some(), ARGS.auth_username.is_some(),
HttpAuthentication::basic(auth_validator), HttpAuthentication::basic(util::auth::auth_validator),
)) ))
}) })
.bind(format!("0.0.0.0:{}", args.port.to_string()))? .bind((ARGS.bind, ARGS.port))?
.workers(args.threads as usize) .workers(ARGS.threads as usize)
.run() .run()
.await .await
} }
fn remove_expired(pastas: &mut Vec<Pasta>) {
// get current time - this will be needed to check which pastas have expired
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(_) => panic!("SystemTime before UNIX EPOCH!"),
} as i64;
pastas.retain(|p| {
// expiration is `never` or not reached
if p.expiration == 0 || p.expiration > timenow {
// keep
true
} else {
// remove the file itself
fs::remove_file(format!("./pasta_data/{}/{}", p.id_as_animals(), p.file));
// and remove the containing directory
fs::remove_dir(format!("./pasta_data/{}/", p.id_as_animals()));
// remove
false
}
});
}
fn is_valid_url(url: &str) -> bool {
let finder = LinkFinder::new();
let spans: Vec<_> = finder.spans(url).collect();
spans[0].as_str() == url && Some(&LinkKind::Url) == spans[0].kind()
}

View file

@ -1,29 +1,74 @@
use std::fmt; use bytesize::ByteSize;
use chrono::{Datelike, Local, TimeZone, Timelike};
use chrono::{DateTime, Datelike, NaiveDateTime, Timelike, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::to_animal_names; use crate::args::ARGS;
use crate::util::hashids::to_hashids;
use crate::util::pasta_id_converter::CONVERTER;
use crate::util::syntaxhighlighter::html_highlight;
#[derive(Serialize, Deserialize, PartialEq, Eq)]
pub struct PastaFile {
pub name: String,
pub size: ByteSize,
}
impl PastaFile {
pub fn from_unsanitized(path: &str) -> Result<Self, &'static str> {
let path = Path::new(path);
let name = path.file_name().ok_or("Path did not contain a file name")?;
let name = name.to_string_lossy().replace(' ', "_");
Ok(Self {
name,
size: ByteSize::b(0),
})
}
pub fn name(&self) -> &str {
&self.name
}
/// Check if file is an image for embedding
pub fn is_image(&self) -> bool {
let guess = mime_guess::from_path(&self.name).first_or_text_plain();
guess.type_() == "image"
}
}
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct Pasta { pub struct Pasta {
pub id: u64, pub id: u64,
pub content: String, pub content: String,
pub file: String, pub file: Option<PastaFile>,
pub extension: String,
pub private: bool,
pub editable: bool,
pub created: i64, pub created: i64,
pub expiration: i64, pub expiration: i64,
pub last_read: i64,
pub read_count: u64,
pub burn_after_reads: u64,
// what types can there be?
// `url`, `text`,
pub pasta_type: String, pub pasta_type: String,
} }
impl Pasta { impl Pasta {
pub fn id_as_animals(&self) -> String { pub fn id_as_animals(&self) -> String {
to_animal_names(self.id) if ARGS.hash_ids {
to_hashids(self.id)
} else {
CONVERTER.to_names(self.id)
}
} }
pub fn created_as_string(&self) -> String { pub fn created_as_string(&self) -> String {
let date = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(self.created, 0), Utc); let date = Local.timestamp(self.created, 0);
format!( format!(
"{:02}-{:02} {}:{}", "{:02}-{:02} {:02}:{:02}",
date.month(), date.month(),
date.day(), date.day(),
date.hour(), date.hour(),
@ -35,10 +80,9 @@ impl Pasta {
if self.expiration == 0 { if self.expiration == 0 {
String::from("Never") String::from("Never")
} else { } else {
let date = let date = Local.timestamp(self.expiration, 0);
DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(self.expiration, 0), Utc);
format!( format!(
"{:02}-{:02} {}:{}", "{:02}-{:02} {:02}:{:02}",
date.month(), date.month(),
date.day(), date.day(),
date.hour(), date.hour(),
@ -46,6 +90,73 @@ impl Pasta {
) )
} }
} }
pub fn last_read_time_ago_as_string(&self) -> String {
// get current unix time in seconds
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(_) => {
log::error!("SystemTime before UNIX EPOCH!");
0
}
} as i64;
// get seconds since last read and convert it to days
let days = ((timenow - self.last_read) / 86400) as u16;
if days > 1 {
return format!("{days} days ago");
};
// it's less than 1 day, let's do hours then
let hours = ((timenow - self.last_read) / 3600) as u16;
if hours > 1 {
return format!("{hours} hours ago");
};
// it's less than 1 hour, let's do minutes then
let minutes = ((timenow - self.last_read) / 60) as u16;
if minutes > 1 {
return format!("{minutes} minutes ago");
};
// it's less than 1 minute, let's do seconds then
let seconds = (timenow - self.last_read) as u16;
if seconds > 1 {
return format!("{seconds} seconds ago");
};
// it's less than 1 second?????
String::from("just now")
}
pub fn last_read_days_ago(&self) -> u16 {
// get current unix time in seconds
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(_) => {
log::error!("SystemTime before UNIX EPOCH!");
0
}
} as i64;
// get seconds since last read and convert it to days
((timenow - self.last_read) / 86400) as u16
}
pub fn content_syntax_highlighted(&self) -> String {
html_highlight(&self.content, &self.extension)
}
pub fn content_not_highlighted(&self) -> String {
html_highlight(&self.content, "txt")
}
pub fn content_escaped(&self) -> String {
self.content
.replace('`', "\\`")
.replace('$', "\\$")
.replace('/', "\\/")
}
} }
impl fmt::Display for Pasta { impl fmt::Display for Pasta {

29
src/util/auth.rs Normal file
View file

@ -0,0 +1,29 @@
use actix_web::dev::ServiceRequest;
use actix_web::{error, Error};
use actix_web_httpauth::extractors::basic::BasicAuth;
use crate::args::ARGS;
pub async fn auth_validator(
req: ServiceRequest,
credentials: BasicAuth,
) -> Result<ServiceRequest, Error> {
// check if username matches
if credentials.user_id().as_ref() == ARGS.auth_username.as_ref().unwrap() {
return match ARGS.auth_password.as_ref() {
Some(cred_pass) => match credentials.password() {
None => Err(error::ErrorBadRequest("Invalid login details.")),
Some(arg_pass) => {
if arg_pass == cred_pass {
Ok(req)
} else {
Err(error::ErrorBadRequest("Invalid login details."))
}
}
},
None => Ok(req),
};
} else {
Err(error::ErrorBadRequest("Invalid login details."))
}
}

View file

@ -4,7 +4,7 @@ use std::io::{BufReader, BufWriter};
use crate::Pasta; use crate::Pasta;
static DATABASE_PATH: &'static str = "pasta_data/database.json"; static DATABASE_PATH: &str = "pasta_data/database.json";
pub fn save_to_file(pasta_data: &Vec<Pasta>) { pub fn save_to_file(pasta_data: &Vec<Pasta>) {
let mut file = File::create(DATABASE_PATH); let mut file = File::create(DATABASE_PATH);
@ -14,11 +14,11 @@ pub fn save_to_file(pasta_data: &Vec<Pasta>) {
serde_json::to_writer(writer, &pasta_data).expect("Failed to create JSON writer"); serde_json::to_writer(writer, &pasta_data).expect("Failed to create JSON writer");
} }
Err(_) => { Err(_) => {
log::info!("Database file {} not found!", DATABASE_PATH); log::info!("Database file {DATABASE_PATH} not found!");
file = File::create(DATABASE_PATH); file = File::create(DATABASE_PATH);
match file { match file {
Ok(_) => { Ok(_) => {
log::info!("Database file {} created.", DATABASE_PATH); log::info!("Database file {DATABASE_PATH} created.");
save_to_file(pasta_data); save_to_file(pasta_data);
} }
Err(err) => { Err(err) => {
@ -27,7 +27,7 @@ pub fn save_to_file(pasta_data: &Vec<Pasta>) {
&DATABASE_PATH, &DATABASE_PATH,
&err &err
); );
panic!("Failed to create database file {}: {}!", DATABASE_PATH, err) panic!("Failed to create database file {DATABASE_PATH}: {err}!")
} }
} }
} }
@ -39,14 +39,17 @@ pub fn load_from_file() -> io::Result<Vec<Pasta>> {
match file { match file {
Ok(_) => { Ok(_) => {
let reader = BufReader::new(file.unwrap()); let reader = BufReader::new(file.unwrap());
let data: Vec<Pasta> = serde_json::from_reader(reader).unwrap(); let data: Vec<Pasta> = match serde_json::from_reader(reader) {
Ok(t) => t,
_ => Vec::new(),
};
Ok(data) Ok(data)
} }
Err(_) => { Err(_) => {
log::info!("Database file {} not found!", DATABASE_PATH); log::info!("Database file {DATABASE_PATH} not found!");
save_to_file(&Vec::<Pasta>::new()); save_to_file(&Vec::<Pasta>::new());
log::info!("Database file {} created.", DATABASE_PATH); log::info!("Database file {DATABASE_PATH} created.");
load_from_file() load_from_file()
} }
} }

18
src/util/hashids.rs Normal file
View file

@ -0,0 +1,18 @@
use harsh::Harsh;
use lazy_static::lazy_static;
lazy_static! {
pub static ref HARSH: Harsh = Harsh::builder().length(6).build().unwrap();
}
pub fn to_hashids(number: u64) -> String {
HARSH.encode(&[number])
}
pub fn to_u64(hash_id: &str) -> Result<u64, &str> {
let ids = HARSH
.decode(hash_id)
.map_err(|_e| "Failed to decode hash ID")?;
let id = ids.first().ok_or("No ID found in hash ID")?;
Ok(*id)
}

66
src/util/misc.rs Normal file
View file

@ -0,0 +1,66 @@
use std::time::{SystemTime, UNIX_EPOCH};
use crate::args::ARGS;
use linkify::{LinkFinder, LinkKind};
use qrcode_generator::QrCodeEcc;
use std::fs;
use crate::{dbio, Pasta};
pub fn remove_expired(pastas: &mut Vec<Pasta>) {
// get current time - this will be needed to check which pastas have expired
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(_) => {
log::error!("SystemTime before UNIX EPOCH!");
0
}
} as i64;
pastas.retain(|p| {
// keep if:
// expiration is `never` or not reached
// AND
// read count is less than burn limit, or no limit set
// AND
// has been read in the last N days where N is the arg --gc-days OR N is 0 (no GC)
if (p.expiration == 0 || p.expiration > timenow)
&& (p.read_count < p.burn_after_reads || p.burn_after_reads == 0)
&& (p.last_read_days_ago() < ARGS.gc_days || ARGS.gc_days == 0)
{
// keep
true
} else {
// remove the file itself
if let Some(file) = &p.file {
if fs::remove_file(format!(
"./pasta_data/public/{}/{}",
p.id_as_animals(),
file.name()
))
.is_err()
{
log::error!("Failed to delete file {}!", file.name())
}
// and remove the containing directory
if fs::remove_dir(format!("./pasta_data/public/{}/", p.id_as_animals())).is_err() {
log::error!("Failed to delete directory {}!", file.name())
}
}
false
}
});
dbio::save_to_file(pastas);
}
pub fn string_to_qr_svg(str: &str) -> String {
qrcode_generator::to_svg_to_string(str, QrCodeEcc::Low, 256, None::<&str>).unwrap()
}
pub fn is_valid_url(url: &str) -> bool {
let finder = LinkFinder::new();
let spans: Vec<_> = finder.spans(url).collect();
spans[0].as_str() == url && Some(&LinkKind::Url) == spans[0].kind()
}

View file

@ -0,0 +1,98 @@
use std::fs;
use lazy_static::lazy_static;
use crate::args::ARGS;
const ANIMAL_NAMES: &[&str] = &[
"ant", "eel", "mole", "sloth", "ape", "emu", "monkey", "snail", "bat", "falcon", "mouse",
"snake", "bear", "fish", "otter", "spider", "bee", "fly", "parrot", "squid", "bird", "fox",
"panda", "swan", "bison", "frog", "pig", "tiger", "camel", "gecko", "pigeon", "toad", "cat",
"goat", "pony", "turkey", "cobra", "goose", "pug", "turtle", "crow", "hawk", "rabbit", "viper",
"deer", "horse", "rat", "wasp", "dog", "jaguar", "raven", "whale", "dove", "koala", "seal",
"wolf", "duck", "lion", "shark", "worm", "eagle", "lizard", "sheep", "zebra",
];
lazy_static!{
pub static ref CONVERTER: PastaIdConverter = PastaIdConverter::new();
}
/// Convert pasta IDs to names and vice versa
pub struct PastaIdConverter {
names: Vec<String>
}
impl PastaIdConverter {
pub fn new() -> Self {
let names;
if let Some(names_path) = &ARGS.custom_names {
let names_data = fs::read_to_string(names_path)
.expect("path for the names file should contain a names file");
names = names_data
.split('\n')
.map(ToOwned::to_owned)
.collect::<Vec<String>>();
} else {
names = ANIMAL_NAMES
.iter()
.copied()
.map(ToOwned::to_owned)
.collect();
}
Self { names }
}
pub fn to_names(&self, mut number: u64) -> String {
let mut result: Vec<&str> = Vec::new();
if number == 0 {
return self.names[0].parse().unwrap();
}
let mut power = 6;
loop {
let digit = number / self.names.len().pow(power) as u64;
if !(result.is_empty() && digit == 0) {
result.push(&self.names[digit as usize]);
}
number -= digit * self.names.len().pow(power) as u64;
if power > 0 {
power -= 1;
} else if power == 0 || number == 0 {
break;
}
}
result.join("-")
}
pub fn to_u64(&self, pasta_id: &str) -> Result<u64, &str> {
let mut result: u64 = 0;
let names: Vec<&str> = pasta_id.split('-').collect();
let mut pow = names.len();
for name in names {
pow -= 1;
let name_index = self.names.iter().position(|r| r == name);
match name_index {
None => return Err("Failed to convert animal name to u64!"),
Some(_) => {
result += (name_index.unwrap() * self.names.len().pow(pow as u32)) as u64
}
}
}
Ok(result)
}
}
impl Default for PastaIdConverter {
fn default() -> Self {
Self::new()
}
}

View file

@ -0,0 +1,37 @@
use syntect::easy::HighlightLines;
use syntect::highlighting::{Style, ThemeSet};
use syntect::html::append_highlighted_html_for_styled_line;
use syntect::html::IncludeBackground::No;
use syntect::parsing::SyntaxSet;
use syntect::util::LinesWithEndings;
pub fn html_highlight(text: &str, extension: &str) -> String {
let ps = SyntaxSet::load_defaults_newlines();
let ts = ThemeSet::load_defaults();
let syntax = ps
.find_syntax_by_extension(extension)
.or_else(|| Option::from(ps.find_syntax_plain_text()))
.unwrap();
let mut h = HighlightLines::new(syntax, &ts.themes["InspiredGitHub"]);
let mut highlighted_content: String = String::from("");
for line in LinesWithEndings::from(text) {
let ranges: Vec<(Style, &str)> = h.highlight_line(line, &ps).unwrap();
append_highlighted_html_for_styled_line(&ranges[..], No, &mut highlighted_content)
.expect("Failed to append highlighted line!");
}
let mut highlighted_content2: String = String::from("");
for line in highlighted_content.lines() {
highlighted_content2 += &*format!("<code-line>{line}</code-line>\n");
}
// Rewrite colours to ones that are compatible with water.css and both light/dark modes
highlighted_content2 = highlighted_content2.replace("style=\"color:#323232;\"", "");
highlighted_content2 =
highlighted_content2.replace("style=\"color:#183691;\"", "style=\"color:blue;\"");
highlighted_content2
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="31.999998"
height="31.999998"
viewBox="0 0 8.4666661 8.4666661"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="logo.svg"
inkscape:export-filename="logo.png"
inkscape:export-xdpi="384"
inkscape:export-ydpi="384"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#4a4a55"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:document-units="mm"
showgrid="true"
inkscape:zoom="19.556004"
inkscape:cx="6.0339524"
inkscape:cy="16.721207"
inkscape:window-width="2560"
inkscape:window-height="1036"
inkscape:window-x="0"
inkscape:window-y="44"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid182"
visible="true" />
</sodipodi:namedview>
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g14438"
inkscape:label="Box">
<path
style="color:#000000;fill:#f7f7ff;-inkscape-stroke:none"
d="M 1.984375,3.8066406 V 6.2207031 L 4.2324219,7.34375 4.2929687,7.3144531 6.4824219,6.2207031 V 3.8066406 l -2.25,1.125 z m 0.2636719,0.4296875 1.984375,0.9921875 1.984375,-0.9921875 V 6.0566406 L 4.2324219,7.0488281 2.2480469,6.0566406 Z"
id="path2056" />
<path
id="path3512"
style="color:#000000;fill:#f7f7ff;fill-opacity:1;-inkscape-stroke:none"
d="M 4.3651082,5.3087199 4.2322998,5.3748657 4.1015584,5.3097534 v 1.5260051 l 0.1328085,0.066663 0.1307413,-0.065629 z" />
</g>
<g
id="g18197"
inkscape:label="Cat"
style="display:inline">
<path
id="path14440"
style="color:#000000;display:inline;fill:#f7f7ff;fill-opacity:1;-inkscape-stroke:none"
d="M 4.6663818,2.8716593 4.3862956,3.0959351 h 0.045475 L 5.4063883,3.291272 5.709729,4.0462646 5.9464071,3.9279256 5.5996582,3.058728 Z M 2.6199951,2.9434896 2.4618652,3.8989868 2.7088786,4.0230103 2.8571899,3.1331421 Z" />
<path
style="color:#000000;fill:#f7f7ff;-inkscape-stroke:none"
d="m 2.1308594,1.0234375 0.2773437,1.109375 v 0.4707031 l 1.1894532,0.953125 0.083984,-0.066406 1.1074219,-0.8867188 V 2.1328125 L 5.0664062,1.0234375 3.9902344,1.5605469 H 3.2070313 Z M 2.5273438,1.5175781 3.1445313,1.8261719 H 4.0527344 L 4.6699219,1.5175781 4.5234375,2.0996094 V 2.4765625 L 3.5976563,3.2167969 2.671875,2.4765625 V 2.0996094 Z"
id="path14903" />
</g>
<path
id="path22977"
style="color:#000000;fill:#f7f7ff;fill-opacity:1;-inkscape-stroke:none"
d="M 5.94434,3.5646403 6.0657796,3.8684977 6.3086589,3.7470581 Z m -3.5723918,0.074414 -0.2149739,0.1074869 0.181901,0.090951 z"
sodipodi:nodetypes="cccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
templates/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

81
templates/assets/logo.svg Normal file
View file

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="32"
height="32"
viewBox="0 0 8.4666666 8.4666666"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="microbin-logo.svg"
inkscape:export-filename="microbin-logo-exp.svg"
inkscape:export-xdpi="144"
inkscape:export-ydpi="144"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#4a4a55"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:document-units="mm"
showgrid="true"
inkscape:zoom="19.556004"
inkscape:cx="6.0339524"
inkscape:cy="16.721207"
inkscape:window-width="2560"
inkscape:window-height="1036"
inkscape:window-x="0"
inkscape:window-y="44"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid182"
visible="true" />
</sodipodi:namedview>
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g14438"
inkscape:label="Box">
<path
style="color:#000000;fill:#f7f7ff;-inkscape-stroke:none"
d="M 1.984375,3.8066406 V 6.2207031 L 4.2324219,7.34375 4.2929687,7.3144531 6.4824219,6.2207031 V 3.8066406 l -2.25,1.125 z m 0.2636719,0.4296875 1.984375,0.9921875 1.984375,-0.9921875 V 6.0566406 L 4.2324219,7.0488281 2.2480469,6.0566406 Z"
id="path2056" />
<path
id="path3512"
style="color:#000000;fill:#f7f7ff;-inkscape-stroke:none;fill-opacity:1"
d="M 4.3651082 5.3087199 L 4.2322998 5.3748657 L 4.1015584 5.3097534 L 4.1015584 6.8357585 L 4.2343669 6.9024211 L 4.3651082 6.836792 L 4.3651082 5.3087199 z " />
</g>
<g
id="g18197"
inkscape:label="Cat"
style="display:inline">
<path
id="path14440"
style="color:#000000;display:inline;fill:#f7f7ff;-inkscape-stroke:none;fill-opacity:1"
d="M 4.6663818,2.8716593 4.3862956,3.0959351 h 0.045475 L 5.4063883,3.291272 5.709729,4.0462646 5.9464071,3.9279256 5.5996582,3.058728 Z M 2.6199951,2.9434896 2.4618652,3.8989868 2.7088786,4.0230103 2.8571899,3.1331421 Z" />
<path
style="color:#000000;fill:#f7f7ff;-inkscape-stroke:none"
d="m 2.1308594,1.0234375 0.2773437,1.109375 v 0.4707031 l 1.1894532,0.953125 0.083984,-0.066406 1.1074219,-0.8867188 V 2.1328125 L 5.0664062,1.0234375 3.9902344,1.5605469 H 3.2070313 Z M 2.5273438,1.5175781 3.1445313,1.8261719 H 4.0527344 L 4.6699219,1.5175781 4.5234375,2.0996094 V 2.4765625 L 3.5976563,3.2167969 2.671875,2.4765625 V 2.0996094 Z"
id="path14903" />
</g>
<path
id="path22977"
style="color:#000000;fill:#f7f7ff;-inkscape-stroke:none;fill-opacity:1"
d="M 5.94434,3.5646403 6.0657796,3.8684977 6.3086589,3.7470581 Z m -3.5723918,0.074414 -0.2149739,0.1074869 0.181901,0.090951 z"
sodipodi:nodetypes="cccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

626
templates/assets/water.css Normal file
View file

@ -0,0 +1,626 @@
/*
* This is (basically) water.css.
*
* repo: https://github.com/kognise/water.css
*
* The license:
*
* The MIT License (MIT)
*
* Copyright © 2019 Kognise
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the Software), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
:root {
--background-body:#4a4a55;
--background:#383844;
--background-alt:#242438;
--selection:#23bf7c;
--text-main:#dfdfef;
--text-bright:#f7f7ff;
--text-muted:#878797;
--links:#28db8f;
--focus:#299465df;
--border:#676773;
--code:var(--text-main);
--animation-duration:0.1s;
--button-base:#299465;
--button-hover:#23bf7c;
--scrollbar-thumb:var(--button-hover);
--scrollbar-thumb-hover:#000;
--form-placeholder:#a9a9a9;
--form-text:#fff;
--variable:#d941e2;
--highlight:#efdb43;
--select-arrow:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='63' width='117' fill='%23efefef'%3E%3Cpath d='M115 2c-1-2-4-2-5 0L59 53 7 2a4 4 0 00-5 5l54 54 2 2 3-2 54-54c2-1 2-4 0-5z'/%3E%3C/svg%3E")
}
html {
scrollbar-color:#040a0f #202b38;
scrollbar-color:var(--scrollbar-thumb) var(--background-body);
scrollbar-width:thin
}
body {
font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,Segoe UI Emoji,Apple Color Emoji,Noto Color Emoji,sans-serif;
line-height:1.4;
max-width:800px;
margin:20px auto;
padding:0 10px;
word-wrap:break-word;
color:#dbdbdb;
color:var(--text-main);
background:#202b38;
background:var(--background-body);
text-rendering:optimizeLegibility
}
button,
input,
textarea {
transition:background-color .1s linear,border-color .1s linear,color .1s linear,box-shadow .1s linear,transform .1s ease;
transition:background-color var(--animation-duration) linear,border-color var(--animation-duration) linear,color var(--animation-duration) linear,box-shadow var(--animation-duration) linear,transform var(--animation-duration) ease
}
h1 {
font-size:2.2em;
margin-top:0
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin-bottom:12px;
margin-top:24px
}
h1,
h2,
h3,
h4,
h5,
h6,
strong {
color:#fff;
color:var(--text-bright)
}
b,
h1,
h2,
h3,
h4,
h5,
h6,
strong,
th {
font-weight:600
}
q:after,
q:before {
content:none
}
blockquote,
q {
border-left:4px solid rgba(0,150,191,.67);
border-left:4px solid var(--focus);
margin:1.5em 0;
padding:.5em 1em;
font-style:italic
}
blockquote>footer {
font-style:normal;
border:0
}
address,
blockquote cite {
font-style:normal
}
a[href^=mailto\:]:before {
content:"📧 "
}
a[href^=tel\:]:before {
content:"📞 "
}
a[href^=sms\:]:before {
content:"💬 "
}
mark {
background-color:#efdb43;
background-color:var(--highlight);
border-radius:2px;
padding:0 2px;
color:#000
}
a>code,
a>strong {
color:inherit
}
button,
input[type=button],
input[type=checkbox],
input[type=radio],
input[type=range],
input[type=reset],
input[type=submit],
select {
cursor:pointer
}
input,
select {
display:block
}
[type=checkbox],
[type=radio] {
display:initial
}
button,
input,
select,
textarea {
color:#fff;
color:var(--form-text);
background-color:#161f27;
background-color:var(--background);
font-family:inherit;
font-size:inherit;
margin-right:6px;
margin-bottom:6px;
padding:10px;
border:none;
border-radius:6px;
outline:none
}
button,
input[type=button],
input[type=reset],
input[type=submit] {
background-color:#0c151c;
background-color:var(--button-base);
padding-right:30px;
padding-left:30px
}
button:hover,
input[type=button]:hover,
input[type=reset]:hover,
input[type=submit]:hover {
background:#040a0f;
background:var(--button-hover)
}
input[type=color] {
min-height:2rem;
padding:8px;
cursor:pointer
}
input[type=checkbox],
input[type=radio] {
height:1em;
width:1em
}
input[type=radio] {
border-radius:100%
}
input {
vertical-align:top
}
label {
vertical-align:middle;
margin-bottom:4px;
display:inline-block
}
button,
input:not([type=checkbox]):not([type=radio]),
input[type=range],
select,
textarea {
-webkit-appearance:none
}
textarea {
display:block;
margin-right:0;
box-sizing:border-box;
resize:vertical
}
textarea:not([cols]) {
width:100%
}
textarea:not([rows]) {
min-height:40px;
height:140px
}
select {
background:#161f27 url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='63' width='117' fill='%23efefef'%3E%3Cpath d='M115 2c-1-2-4-2-5 0L59 53 7 2a4 4 0 00-5 5l54 54 2 2 3-2 54-54c2-1 2-4 0-5z'/%3E%3C/svg%3E") calc(100% - 12px) 50%/12px no-repeat;
background:var(--background) var(--select-arrow) calc(100% - 12px) 50%/12px no-repeat;
padding-right:35px
}
select::-ms-expand {
display:none
}
select[multiple] {
padding-right:10px;
background-image:none;
overflow-y:auto
}
button:focus,
input:focus,
select:focus,
textarea:focus {
box-shadow:0 0 0 2px rgba(0,150,191,.67);
box-shadow:0 0 0 2px var(--focus)
}
button:active,
input[type=button]:active,
input[type=checkbox]:active,
input[type=radio]:active,
input[type=range]:active,
input[type=reset]:active,
input[type=submit]:active {
transform:translateY(2px)
}
button:disabled,
input:disabled,
select:disabled,
textarea:disabled {
cursor:not-allowed;
opacity:.5
}
::-moz-placeholder {
color:#a9a9a9;
color:var(--form-placeholder)
}
:-ms-input-placeholder {
color:#a9a9a9;
color:var(--form-placeholder)
}
::-ms-input-placeholder {
color:#a9a9a9;
color:var(--form-placeholder)
}
::placeholder {
color:#a9a9a9;
color:var(--form-placeholder)
}
fieldset {
border:1px solid rgba(0,150,191,.67);
border:1px solid var(--focus);
border-radius:6px;
margin:0 0 12px;
padding:10px
}
legend {
font-size:.9em;
font-weight:600
}
input[type=range] {
margin:10px 0;
padding:10px 0;
background:transparent
}
input[type=range]:focus {
outline:none
}
input[type=range]::-webkit-slider-runnable-track {
width:100%;
height:9.5px;
-webkit-transition:.2s;
transition:.2s;
background:#161f27;
background:var(--background);
border-radius:3px
}
input[type=range]::-webkit-slider-thumb {
box-shadow:0 1px 1px #000,0 0 1px #0d0d0d;
height:20px;
width:20px;
border-radius:50%;
background:#526980;
background:var(--border);
-webkit-appearance:none;
margin-top:-7px
}
input[type=range]:focus::-webkit-slider-runnable-track {
background:#161f27;
background:var(--background)
}
input[type=range]::-moz-range-track {
width:100%;
height:9.5px;
-moz-transition:.2s;
transition:.2s;
background:#161f27;
background:var(--background);
border-radius:3px
}
input[type=range]::-moz-range-thumb {
box-shadow:1px 1px 1px #000,0 0 1px #0d0d0d;
height:20px;
width:20px;
border-radius:50%;
background:#526980;
background:var(--border)
}
input[type=range]::-ms-track {
width:100%;
height:9.5px;
background:transparent;
border-color:transparent;
border-width:16px 0;
color:transparent
}
input[type=range]::-ms-fill-lower,
input[type=range]::-ms-fill-upper {
background:#161f27;
background:var(--background);
border:.2px solid #010101;
border-radius:3px;
box-shadow:1px 1px 1px #000,0 0 1px #0d0d0d
}
input[type=range]::-ms-thumb {
box-shadow:1px 1px 1px #000,0 0 1px #0d0d0d;
border:1px solid #000;
height:20px;
width:20px;
border-radius:50%;
background:#526980;
background:var(--border)
}
input[type=range]:focus::-ms-fill-lower,
input[type=range]:focus::-ms-fill-upper {
background:#161f27;
background:var(--background)
}
a {
text-decoration:none;
color:#41adff;
color:var(--links)
}
a:hover {
text-decoration:underline
}
code,
samp,
time {
background:#161f27;
background:var(--background);
color:#ffbe85;
color:var(--code);
padding:2.5px 5px;
border-radius:6px;
font-size:1em
}
pre>code {
padding:10px;
display:block;
overflow-x:auto
}
var {
color:#d941e2;
color:var(--variable);
font-style:normal;
font-family:monospace
}
kbd {
background:#161f27;
background:var(--background);
border:1px solid #526980;
border:1px solid var(--border);
border-radius:2px;
color:#dbdbdb;
color:var(--text-main);
padding:2px 4px
}
img,
video {
max-width:100%;
height:auto
}
hr {
border:none;
border-top:1px solid #526980;
border-top:1px solid var(--border)
}
table {
border-collapse:collapse;
margin-bottom:10px;
width:100%;
table-layout:fixed
}
table caption,
td,
th {
text-align:left
}
td,
th {
padding:6px;
vertical-align:top;
word-wrap:break-word
}
thead {
border-bottom:1px solid #526980;
border-bottom:1px solid var(--border)
}
tfoot {
border-top:1px solid #526980;
border-top:1px solid var(--border)
}
tbody tr:nth-child(2n) {
background-color:#161f27;
background-color:var(--background)
}
tbody tr:nth-child(2n) button {
background-color:#1a242f;
background-color:var(--background-alt)
}
tbody tr:nth-child(2n) button:hover {
background-color:#202b38;
background-color:var(--background-body)
}
::-webkit-scrollbar {
height:10px;
width:10px
}
::-webkit-scrollbar-track {
background:#161f27;
background:var(--background);
border-radius:6px
}
::-webkit-scrollbar-thumb {
background:#040a0f;
background:var(--scrollbar-thumb);
border-radius:6px
}
::-webkit-scrollbar-thumb:hover {
background:#000;
background:var(--scrollbar-thumb-hover)
}
::-moz-selection {
background-color:#1c76c5;
background-color:var(--selection);
color:#fff;
color:var(--text-bright)
}
::selection {
background-color:#1c76c5;
background-color:var(--selection);
color:#fff;
color:var(--text-bright)
}
details {
display:flex;
flex-direction:column;
align-items:flex-start;
background-color:#1a242f;
background-color:var(--background-alt);
padding:10px 10px 0;
margin:1em 0;
border-radius:6px;
overflow:hidden
}
details[open] {
padding:10px
}
details>:last-child {
margin-bottom:0
}
details[open] summary {
margin-bottom:10px
}
summary {
display:list-item;
background-color:#161f27;
background-color:var(--background);
padding:10px;
margin:-10px -10px 0;
cursor:pointer;
outline:none
}
summary:focus,
summary:hover {
text-decoration:underline
}
details>:not(summary) {
margin-top:0
}
summary::-webkit-details-marker {
color:#dbdbdb;
color:var(--text-main)
}
dialog {
background-color:#1a242f;
background-color:var(--background-alt);
color:#dbdbdb;
color:var(--text-main);
border-radius:6px;
border:#526980;
border-color:var(--border);
padding:10px 30px
}
dialog>header:first-child {
background-color:#161f27;
background-color:var(--background);
border-radius:6px 6px 0 0;
margin:-10px -30px 10px;
padding:10px;
text-align:center
}
dialog::-webkit-backdrop {
background:rgba(0,0,0,.61);
-webkit-backdrop-filter:blur(4px);
backdrop-filter:blur(4px)
}
dialog::backdrop {
background:rgba(0,0,0,.61);
-webkit-backdrop-filter:blur(4px);
backdrop-filter:blur(4px)
}
footer {
border-top:1px solid #526980;
border-top:1px solid var(--border);
padding-top:10px;
color:#a9b1ba;
color:var(--text-muted)
}
body>footer {
margin-top:40px
}
@media print {
body,
button,
code,
details,
input,
pre,
summary,
textarea {
background-color:#fff
}
button,
input,
textarea {
border:1px solid #000
}
body,
button,
code,
footer,
h1,
h2,
h3,
h4,
h5,
h6,
input,
pre,
strong,
summary,
textarea {
color:#000
}
summary::marker {
color:#000
}
summary::-webkit-details-marker {
color:#000
}
tbody tr:nth-child(2n) {
background-color:#f2f2f2
}
a {
color:#00f;
text-decoration:underline
}
}

20
templates/edit.html Normal file
View file

@ -0,0 +1,20 @@
{% include "header.html" %}
<form method="POST" enctype="multipart/form-data">
<h4>
Editing pasta '{{ pasta.id_as_animals() }}'
</h4>
<label>Content</label>
<br>
<textarea style="width: 100%; min-height: 100px" name="content" id="content" autofocus>{{ pasta.content }}</textarea>
<br>
{% if args.readonly %}
<input style="width: 140px; background-color: limegreen" disabled type="submit" value="Read Only"/>
{%- else %}
<input style="width: 140px; background-color: limegreen" type="submit" value="Save"/>
{%- endif %}
</td>
<br>
</form>
{% include "footer.html" %}

View file

@ -1,10 +1,10 @@
{% include "header.html" %} {% include "header.html" %}
<br> <br>
<h2>404</h2> <h2>{{ status_code.as_u16() }}</h2>
<b>Not Found</b> <b>{{ status_code.canonical_reason().unwrap_or("Unknown error") }}</b>
<br> <br>
<br> <br>
<a href="/" > Go Home</a> <a href="{{ args.public_path }}/"> Go Home</a>
<br> <br>
<br> <br>
{% include "footer.html" %} {% include "footer.html" %}

View file

@ -1,13 +1,18 @@
{% if !args.hide_footer %} {% if !args.hide_footer %}
<hr> <hr>
<p style="font-size: smaller"> <p style="font-size: smaller">
MicroBin by Daniel Szabo. Fork me on <a href="https://github.com/szabodanika/microbin">GitHub</a>! {% if args.footer_text.as_ref().is_none() %}
<b>Karton</b> by Schrottkatze, based on <a href="https://microbin.eu">MicroBin</a> by Dániel Szabó and the FOSS Community.
Let's keep the Web <b>compact</b>, <b>accessible</b> and <b>humane</b>! Let's keep the Web <b>compact</b>, <b>accessible</b> and <b>humane</b>!
{%- else %}
{{ args.footer_text.as_ref().unwrap() }}
{%- endif %}
</p> </p>
{%- endif %} {%- endif %}
</body> </body>
</html>
</html>

View file

@ -1,35 +1,65 @@
<!DOCTYPE html>
<html> <html>
<head> <head>
<title>MicroBin</title> <title>{{ args.title }}</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/svg+xml" href="{{ args.public_path }}/static/favicon.svg">
{% if !args.pure_html %} {% if !args.pure_html %}
<link rel="stylesheet" href="/static/water.css"> {% if args.custom_css.as_ref().is_none() %}
<link rel="stylesheet" href="{{ args.public_path }}/static/water.css">
{%- else %}
<link rel="stylesheet" href="{{ args.custom_css.as_ref().unwrap() }}">
{%- endif %} {%- endif %}
</head> {%- endif %}
</head>
{% if args.wide %}
<body style=" <body style="
max-width: 720px; max-width: 1080px;
margin: auto; margin: auto;
padding-left:0.5rem; padding-left:0.5rem;
padding-right:0.5rem; padding-right:0.5rem;
line-height: 1.5; line-height: 1.5;
font-size: 1.1em; font-size: 1.1em;
"> ">
{%- else %}
<br> <body style="
max-width: 800px;
margin: auto;
padding-left:0.5rem;
padding-right:0.5rem;
line-height: 1.5;
font-size: 1.1em;
">
{%- endif %}
<br>
{% if !args.hide_header %} {% if !args.hide_header %}
<b style="margin-right: 0.5rem"> <b style="margin-right: 0.5rem">
<i><span style="font-size:2.2rem; margin-right:1rem">μ</span></i> MicroBin
</b>
<a href="/" style="margin-right: 0.5rem; margin-left: 0.5rem">New Pasta</a> {% if !args.hide_logo %}
<a href="{{ args.public_path }}/"><img
width=48
style="margin-bottom: -12px;"
src="{{ args.public_path }}/static/logo.png"
></a>
{%- endif %}
{{ args.title }}
</b>
<a href="/pastalist" style="margin-right: 0.5rem; margin-left: 0.5rem">Pasta List</a> <a href="{{ args.public_path }}/" style="margin-right: 0.5rem; margin-left: 0.5rem">New
</a>
<a href="https://github.com/szabodanika/microbin" style="margin-right: 0.5rem; margin-left: 0.5rem">GitHub</a> <a href="{{ args.public_path }}/pastalist" style="margin-right: 0.5rem; margin-left: 0.5rem">List</a>
<hr>
{%- endif %} <a href="{{ args.public_path }}/info" style="margin-right: 0.5rem; margin-left: 0.5rem">Info</a>
<hr>
{%- endif %}

View file

@ -1,27 +1,252 @@
{% include "header.html" %} {% include "header.html" %}
<form action="upload" method="POST" enctype="multipart/form-data">
<br> <form id="pasta-form" action="upload" method="POST" enctype="multipart/form-data">
<label for="expiration">Expiration</label><br>
<select style="width: 100%;" name="expiration" id="expiration">
<optgroup label="Expire">
<option value="1min">1 minute</option>
<option value="10min">10 minutes</option>
<option value="1hour">1 hour</option>
<option selected value="24hour">24 hours</option>
<option value="1week">1 week</option>
</optgroup>
<option value="never">Never Expire</option>
</select>
<br> <br>
<div id="settings">
<div>
<label for="expiration">Expiration</label><br>
<select style="width: 100%;" name="expiration" id="expiration">
<optgroup label="Expire after">
{% if args.default_expiry == "1min" %}
<option selected value="1min">
{%- else %}
<option value="1min">
{%- endif %}
1 minute
</option>
{% if args.default_expiry == "10min" %}
<option selected value="10min">
{%- else %}
<option value="10min">
{%- endif %}
10 minutes
</option>
{% if args.default_expiry == "1hour" %}
<option selected value="1hour">
{%- else %}
<option value="1hour">
{%- endif %}
1 hour
</option>
{% if args.default_expiry == "24hour" %}
<option selected value="24hour">
{%- else %}
<option value="24hour">
{%- endif %}
24 hours
</option>
{% if args.default_expiry == "3days" %}
<option selected value="3days">
{%- else %}
<option value="3days">
{%- endif %}
3 days
</option>
{% if args.default_expiry == "1week" %}
<option selected value="1week">
{%- else %}
<option value="1week">
{%- endif %}
1 week
</option>
</optgroup>
{% if !args.no_eternal_pasta %}
<option value="never">Never Expire</option>
{%- endif %}
</select>
</div>
{% if args.enable_burn_after %}
<div>
<label for="expiration">Burn After</label><br>
<select style="width: 100%;" name="burn_after" id="burn_after">
<optgroup label="Burn after">
{% if args.default_burn_after == 1 %}
<option selected value="1">
{%- else %}
<option value="1">
{%- endif %}
First Read
</option>
{% if args.default_burn_after == 10 %}
<option selected value="10">
{%- else %}
<option value="10">
{%- endif %}
10th Read
</option>
{% if args.default_burn_after == 100 %}
<option selected value="100">
{%- else %}
<option value="100">
{%- endif %}
100th Read
</option>
{% if args.default_burn_after == 1000 %}
<option selected value="1000">
{%- else %}
<option value="1000">
{%- endif %}
1000th Read
</option>
{% if args.default_burn_after == 10000 %}
<option selected value="10000">
{%- else %}
<option value="10000">
{%- endif %}
10000th Read
</option>
</optgroup>
{% if args.default_burn_after == 0 %}
<option selected value="0">
{%- else %}
<option value="0">
{%- endif %}
No Limit
</option>
</select>
</div>
{%- endif %}
{% if args.highlightsyntax %}
<div>
<label for="syntax-highlight">Syntax</label><br>
<select style="width: 100%;" name="syntax-highlight" id="syntax-highlight">
<option value="none">None</option>
<optgroup label="Source Code">
<option value="sh">Bash Shell</option>
<option value="c">C</option>
<option value="cpp">C++</option>
<option value="cs">C#</option>
<option value="pas">Delphi</option>
<option value="erl">Erlang</option>
<option value="go">Go</option>
<option value="hs">Haskell</option>
<option value="html">HTML</option>
<option value="lua">Lua</option>
<option value="lisp">Lisp</option>
<option value="java">Java</option>
<option value="js">JavaScript</option>
<option value="kt">Kotlin</option>
<option value="py">Python</option>
<option value="php">PHP</option>
<option value="r">R</option>
<option value="rs">Rust</option>
<option value="rb">Ruby</option>
<option value="sc">Scala</option>
<option value="swift">Swift</option>
</optgroup>
<optgroup label="Descriptors">
<!-- no toml support ;-( -->
<option value="json">TOML</option>
<option value="yaml">YAML</option>
<option value="json">JSON</option>
<option value="xml">XML</option>
</optgroup>
</select>
</div>
{%- else %}
<input type="hidden" name="syntax-highlight" value="none">
{%- endif %}
<div>
{% if args.editable || args.private %}
<label>Other</label>
{%- endif %}
{% if args.editable %}
<div>
<input type="checkbox" id="editable" name="editable" value="editable">
<label for="editable">Editable</label>
</div>
{%- endif %}
{% if args.private %}
<div>
<input type="checkbox" id="private" name="private" value="private">
<label for="private">Private</label>
</div>
{%- endif %}
</div>
</div>
<label>Content</label> <label>Content</label>
<br> <textarea style="width: 100%; min-height: 100px; margin-bottom: 2em" name="content" autofocus></textarea>
<textarea style="width: 100%; min-height: 100px" name="content" autofocus></textarea> <div style="overflow:auto;">
<br> {% if !args.no_file_upload %}
<label>File attachment</label> <div style="float: left">
<br> <label for="file" id="attach-file-button-label"><a role="button" id="attach-file-button">Select or drop
<input style="width: 100%;" type="file" id="file" name="file"> file attachment</a></label>
<br> <br>
<input style="width: 120px; background-color: limegreen" ; type="submit" value="Save"/> <input type="file" id="file" name="file" />
<br> </div>
{% endif %}
{% if args.readonly %}
<b>
<!--<input style="width: 140px; float: right; background-color: #0076d18f;" disabled type="submit"-->
<!--value="Read Only" /></b>-->
<input style="width: 140px; float: right" disabled type="submit"
value="Read Only" /></b>
{%- else %}
<b>
<!--<input style="width: 140px; float: right; background-color: #0076d18f;" type="submit" value="Save" />-->
<input style="width: 140px; float: right" type="submit" value="Save" />
</b>
{%- endif %}
</div>
</form> </form>
<script>
const hiddenFileButton = document.getElementById('file');
const attachFileButton = document.getElementById('attach-file-button');
const dropContainer = document.getElementById('pasta-form');
hiddenFileButton.addEventListener('change', function () {
attachFileButton.textContent = "Attached: " + this.files[0].name;
});
dropContainer.ondragover = dropContainer.ondragenter = function (evt) {
evt.preventDefault();
if (hiddenFileButton.files.length == 0) {
attachFileButton.textContent = "Drop your file here";
} else {
attachFileButton.textContent = "Drop your file here to replace " + hiddenFileButton.files[0].name;
}
};
dropContainer.ondrop = function (evt) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(evt.dataTransfer.files[0]);
hiddenFileButton.files = dataTransfer.files;
attachFileButton.textContent = "Attached: " + hiddenFileButton.files[0].name;
evt.preventDefault();
};
</script>
<style>
input::file-selector-button {
display: none;
}
#settings {
display: grid;
grid-gap: 6px;
grid-template-columns: repeat(auto-fit, 150px);
grid-template-rows: repeat(1, 90px);
margin-bottom: 0.5rem;
}
select {
height: 3rem;
}
#attach-file-button-label {
padding-top: 1rem;
padding-bottom: 1rem;
cursor: pointer;
}
#file {
display: none;
}
</style>
{% include "footer.html" %} {% include "footer.html" %}

42
templates/info.html Normal file
View file

@ -0,0 +1,42 @@
{% include "header.html" %}
<h2>Welcome to MicroBin</h2>
<div style="height: 200px;">
<div style="float: left">
<h4>Links</h4>
<a href="https://microbin.eu/documentation" style="margin-right: 1rem">Documentation and Help</a>
<br>
<a href="https://gitlab.com/obsidianical/microbin" style="margin-right: 1rem">Source Code</a>
<br>
<a href="https://gitlab.com/obsidianical/microbin/issues" style="margin-right: 1rem">Feedback</a>
<br>
<a href="https://microbin.eu/donate">Donate and Sponsor</a>
</div>
<div style="float: right">
<h4>Info</h4>
<table style="width: 400px">
<tr>
<td><b>Version</b></td>
<td>{{version_string}} </td>
</tr>
<tr>
<td><b>Status</b></td>
<td>{{status}} </td>
</tr>
<tr>
<td><b>Pastas</b></td>
<td>{{pastas.len()}} </td>
</tr>
</table>
</div>
</div>
{% if message != "" %}
<h4>Messages</h4>
<p>{{message}}</p>
{%- endif %}
<br>
{% include "footer.html" %}

View file

@ -1,9 +1,149 @@
{% include "header.html" %} {% include "header.html" %}
<a style="margin-right: 0.5rem" href="/raw/{{pasta.id_as_animals()}}">Raw Text Content</a> <div style="float: left">
{% if pasta.file != "no-file" %} {% if pasta.content != "No Text Content" %}
<a style="margin-right: 0.5rem; margin-left: 0.5rem" href="/file/{{pasta.id_as_animals()}}/{{pasta.file}}">Attached file '{{pasta.file}}'</a> <button id="copy-text-button" class="copy-button" style="margin-right: 0.5rem">
Copy Text
</button>
{% if args.public_path.to_string() != "" && pasta.pasta_type == "url" %}
<button id="copy-redirect-button" class="copy-button" style="margin-right: 0.5rem">
Copy Redirect
</button>
{%- endif %}
<a style="margin-right: 1rem" href="{{ args.public_path }}/{{ args.raw_endpoint }}/{{pasta.id_as_animals()}}">Raw Text
Content</a>
{%- endif %}
{% if args.qr && args.public_path.to_string() != "" %}
<a style="margin-right: 1rem" href="{{ args.public_path }}/qr/{{pasta.id_as_animals()}}">QR</a>
{%- endif %}
{% if pasta.editable %}
<a style="margin-right: 1rem" href="{{ args.public_path }}/edit/{{pasta.id_as_animals()}}">Edit</a>
{%- endif %}
<a style="margin-right: 1rem" href="{{ args.public_path }}/remove/{{pasta.id_as_animals()}}">Remove</a>
</div>
<div style="float: right">
<a style="margin-right: 0.5rem"
href="{{ args.public_path }}/{{ args.pasta_endpoint }}/{{pasta.id_as_animals()}}"><i>{{pasta.id_as_animals()}}</i></a>
{% if args.public_path.to_string() != "" %}
<button id="copy-url-button" class="copy-button" style="margin-right: 0">
Copy URL
</button>
{%- endif %}
</div>
{% if pasta.file.is_some() %}
<br>
<br>
{% if pasta.file.as_ref().unwrap().is_image() %}
<img src="{{ args.public_path }}/file/{{pasta.id_as_animals()}}/{{pasta.file.as_ref().unwrap().name()}}" alt="">
<br>
{%- endif %} {%- endif %}
<a style="margin-right: 0.5rem; margin-left: 0.5rem" href="/remove/{{pasta.id_as_animals()}}">Remove</a> <a href="{{ args.public_path }}/file/{{pasta.id_as_animals()}}/{{pasta.file.as_ref().unwrap().name()}}" download>
<pre><code>{{pasta}}</code></pre> Download attached file: '{{pasta.file.as_ref().unwrap().name()}}' [{{pasta.file.as_ref().unwrap().size}}]
</a>
{%- endif %}
<br>
<br>
{% if pasta.content != "No Text Content" %}
<div class="code-container">
<div style="clear: both;">
{% if args.highlightsyntax %}
<pre><code id="code">{{pasta.content_syntax_highlighted()}}</code></pre>
{%- else %}
<pre><code id="code">{{pasta.content_not_highlighted()}}</code></pre>
{%- endif %}
</div>
</div>
{%- endif %}
<div>
{% if pasta.read_count == 1 %}
<p style="font-size: small">Read {{pasta.read_count}} time, last {{pasta.last_read_time_ago_as_string()}}</p>
{%- else %}
<p style="font-size: small">Read {{pasta.read_count}} times, last {{pasta.last_read_time_ago_as_string()}}</p>
{%- endif %}
</div>
<br>
<script>
const copyURLBtn = document.getElementById("copy-url-button")
const copyTextBtn = document.getElementById("copy-text-button")
const copyRedirectBtn = document.getElementById("copy-redirect-button")
const content = `{{ pasta.content_escaped() }}`
const url = `{{ args.public_path }}/{{ args.pasta_endpoint }}/{{pasta.id_as_animals()}}`
const redirect_url = `{{ args.public_path }}/{{ args.url_endpoint }}/{{pasta.id_as_animals()}}`
copyURLBtn.addEventListener("click", () => {
navigator.clipboard.writeText(url)
copyURLBtn.innerHTML = "Copied"
setTimeout(() => {
copyURLBtn.innerHTML = "Copy URL"
}, 1000)
})
// it will be undefined when the element does not exist on non-url pastas
if (copyRedirectBtn) {
copyRedirectBtn.addEventListener("click", () => {
navigator.clipboard.writeText(redirect_url)
copyRedirectBtn.innerHTML = "Copied"
setTimeout(() => {
copyRedirectBtn.innerHTML = "Copy Redirect"
}, 1000)
})
}
copyTextBtn.addEventListener("click", () => {
navigator.clipboard.writeText(content)
copyTextBtn.innerHTML = "Copied"
setTimeout(() => {
copyTextBtn.innerHTML = "Copy Text"
}, 1000)
})
</script>
<style>
code-line {
counter-increment: listing;
text-align: right;
float: left;
clear: left;
}
code-line::before {
content: counter(listing);
display: inline-block;
padding-left: auto;
margin-left: auto;
text-align: left;
width: 1.6rem;
border-right: 1px solid lightgrey;
color: grey;
margin-right: 0.4rem;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
#code {
min-height: 2rem;
}
.code-container {
position: relative;
}
.hidden {
display: none;
}
.copy-button {
font-size: small;
padding: 4px;
padding-left: 0.8rem;
padding-right: 0.8rem;
}
</style>
{% include "footer.html" %} {% include "footer.html" %}

View file

@ -4,98 +4,117 @@
{% if pastas.is_empty() %} {% if pastas.is_empty() %}
<br> <br>
<p> <p>
No pastas yet. 😔 Create one <a href="/">here</a>. No pastas yet. 😔 Create one <a href="{{ args.public_path }}/">here</a>.
</p> </p>
<br> <br>
{%- else %} {%- else %}
<br> <h3>Pastas</h3>
<table style="width: 100%"> {% if args.pure_html %}
<thead> <table border="1" style="width: 100%; white-space: nowrap;">
<tr> {% else %}
<th colspan="4">Pastas</th> <table style="width: 100%">
</tr> {% endif %}
<tr> <thead>
<th> <th style="width: 30%">
Key Key
</th> </th>
<th> <th style="width: 20%">
Created Created
</th> </th>
<th> <th style="width: 20%">
Expiration Expiration
</th> </th>
<th> <th style="width: 30%">
</th>
</th> </thead>
</tr> <tbody>
</thead> {% for pasta in pastas %}
<tbody> {% if pasta.pasta_type == "text" && !pasta.private %}
{% for pasta in pastas %} <tr>
{% if pasta.pasta_type == "text" %} <td>
<tr> <a href="{{ args.public_path }}/{{ args.pasta_endpoint }}/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
<td> </td>
<a href="/pasta/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a> <td>
</td> {{pasta.created_as_string()}}
<td> </td>
{{pasta.created_as_string()}} <td>
</td> {{pasta.expiration_as_string()}}
<td> </td>
{{pasta.expiration_as_string()}} <td>
</td> <a style="margin-right:1rem" href="{{ args.public_path }}/{{ args.raw_endpoint }}/{{pasta.id_as_animals()}}">Raw</a>
<td> {% if pasta.file.is_some() %}
<a style="margin-right:1rem" href="/raw/{{pasta.id_as_animals()}}">Raw</a> <a style="margin-right:1rem"
{% if pasta.file != "no-file" %} href="{{ args.public_path }}/file/{{pasta.id_as_animals()}}/{{pasta.file.as_ref().unwrap().name()}}">File</a>
<a style="margin-right:1rem" href="/file/{{pasta.id_as_animals()}}/{{pasta.file}}">File</a> {%- endif %}
{% if pasta.editable %}
<a style="margin-right:1rem" href="{{ args.public_path }}/edit/{{pasta.id_as_animals()}}">Edit</a>
{%- endif %}
<a href="{{ args.public_path }}/remove/{{pasta.id_as_animals()}}">Remove</a>
</td>
</tr>
{%- endif %}
{% endfor %}
</tbody>
</table>
<br>
<h3>URL Redirects</h3>
{% if args.pure_html %}
<table border="1" style="width: 100%">
{% else %}
<table style="width: 100%">
{% endif %}
<thead>
<th style="width: 30%">
Key
</th>
<th style="width: 20%">
Created
</th>
<th style="width: 20%">
Expiration
</th>
<th style="width: 30%">
</th>
</thead>
{% for pasta in pastas %}
{% if pasta.pasta_type == "url" && !pasta.private %}
<tr>
<td>
<a href="{{ args.public_path }}/{{ args.pasta_endpoint }}/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
</td>
<td>
{{pasta.created_as_string()}}
</td>
<td>
{{pasta.expiration_as_string()}}
</td>
<td>
<a style="margin-right:1rem" href="{{ args.public_path }}/{{ args.url_endpoint }}/{{pasta.id_as_animals()}}">Open</a>
<a style="margin-right:1rem; cursor: pointer;" id="copy-button"
data-url="{{ args.public_path }}/{{ args.url_endpoint }}/{{pasta.id_as_animals()}}">Copy</a>
{% if pasta.editable %}
<a style="margin-right:1rem" href="{{ args.public_path }}/edit/{{pasta.id_as_animals()}}">Edit</a>
{%- endif %}
<a href="{{ args.public_path }}/remove/{{pasta.id_as_animals()}}">Remove</a>
</td>
</tr>
{%- endif %}
{% endfor %}
</tbody>
</table>
<br>
{%- endif %} {%- endif %}
<a href="/remove/{{pasta.id_as_animals()}}">Remove</a>
</td>
</tr>
{%- endif %}
{% endfor %}
</tbody>
</table>
<br>
<table>
<thead>
<tr>
<th colspan="4">URL Redirects</th>
</tr>
<tr >
<th>
Key
</th>
<th>
Created
</th>
<th>
Expiration
</th>
<th>
</th> <script>
</tr> const btn = document.querySelector("#copy-button");
</thead> btn.addEventListener("click", () => {
{% for pasta in pastas %} navigator.clipboard.writeText(btn.dataset.url)
{% if pasta.pasta_type == "url" %} btn.innerHTML = "Copied"
<tr> setTimeout(() => {
<td> btn.innerHTML = "Copy"
<a href="/url/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a> }, 1000)
</td> })
<td>
{{pasta.created_as_string()}} </script>
</td>
<td> {% include "footer.html" %}
{{pasta.expiration_as_string()}}
</td>
<td>
<a style="margin-right:1rem" href="/raw/{{pasta.id_as_animals()}}">Raw</a>
<a href="/remove/{{pasta.id_as_animals()}}">Remove</a>
</td>
</tr>
{%- endif %}
{% endfor %}
</tbody>
</table>
<br>
{%- endif %}
{% include "footer.html" %}

29
templates/qr.html Normal file
View file

@ -0,0 +1,29 @@
{% include "header.html" %}
<div style="float: left">
<a href="{{ args.public_path }}/pasta/{{pasta.id_as_animals()}}">Back to Pasta</a>
</div>
<div style="text-align: center; padding: 3rem;">
{% if pasta.pasta_type == "url" %}
<a href="{{ args.public_path }}/url/{{pasta.id_as_animals()}}">
{{qr}}
</a>
{% else %}
<a href="{{ args.public_path }}/pasta/{{pasta.id_as_animals()}}">
{{qr}}
</a>
{% endif %}
</div>
<style>
.copy-text-button,
.copy-url-button {
font-size: small;
padding: 4px;
width: 6rem;
}
</style>
{% include "footer.html" %}