Compare commits

..

3 Commits

Author SHA1 Message Date
Dániel Szabó
907b595339
Update README.MD 2022-06-09 22:58:48 +01:00
Daniel Szabo
16de177083 Merge branch 'master' of https://github.com/szabodanika/microbin 2022-06-09 22:48:25 +01:00
Daniel Szabo
dda65a53e1 - changed how static web resources are served
- fixed sizing consistency for pasta setting fields on index.html
- added new logo
- updated README.MD
2022-06-09 22:48:15 +01:00
25 changed files with 146 additions and 260 deletions

13
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,13 @@
# These are supported funding model platforms
github: szabodanika
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: dani_sz
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@ -1,4 +1,4 @@
name: Rust
name: Build
on:
push:

View File

@ -1,6 +1,6 @@
[package]
name="microbin"
version="1.1.0"
version="1.0.2"
edition="2021"
authors = ["Daniel Szabo <daniel.szabo99@outlook.com>"]
license = "BSD-3-Clause"
@ -18,13 +18,12 @@ actix-web="4"
actix-files="0.6.0"
serde={ version = "1.0", features = ["derive"] }
serde_json = "1.0.80"
bytesize = { version = "1.1", features = ["serde"] }
askama="0.10"
askama-filters={ version = "0.1.3", features = ["chrono"] }
chrono="0.4.19"
rand="0.8.5"
linkify="0.8.1"
clap={ version = "3.1.12", features = ["derive", "env"] }
clap={ version = "3.1.12", features = ["derive"] }
actix-multipart = "0.4.0"
futures = "0.3"
sanitize-filename = "0.3.0"

View File

@ -19,5 +19,8 @@ WORKDIR /usr/local/bin
# copy built exacutable
COPY --from=builder /usr/src/microbin/target/release/microbin /usr/local/bin/microbin
# copy /static folder containing the stylesheets
COPY --from=builder /usr/src/microbin/static /usr/local/bin/static
# run the binary
CMD ["microbin"]
CMD ["microbin"]

View File

@ -1,24 +1,17 @@
![Screenshot](git/index.png)
# MicroBin
# <img src="git/logo.png" alt="Logo" width="35" /> MicroBin
![Build](https://github.com/szabodanika/microbin/actions/workflows/rust.yml/badge.svg)
![crates.io](https://img.shields.io/crates/v/microbin.svg)
MicroBin is a super tiny, feature rich, configurable, self-contained and self-hosted paste bin web application. It is very easy to set up and use, and will only require a few megabytes of memory and disk storage. It takes only a couple minutes to set it up, why not give it a try now?
[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/szabodanika/microbin)
Or install from Cargo:
Or install from Cargo:
`cargo install microbin`, and run with your custom configuration: `microbin --port 8080 --highlightsyntax --editable`.
`cargo install microbin`
And run with your custom configuration:
`microbin --port 8080 --highlightsyntax --editable`
![Screenshot](git/index.png)
### Features
- Is very small
@ -105,14 +98,11 @@ Remember, MicroBin will create your database and file storage wherever you execu
`cd ~/microbin/`
`microbin --highlightsyntax --editable`
### From AUR (for any Arch-based distro)
Install `microbin` package from AUR and start/enable microbin systemd service. Systemd will start server on 127.0.0.1:8080 with almost all features enabled, but this can be changed in `/etc/microbin.conf`.
`microbin --port 8080 --highlightsyntax --editable`
### Building MicroBin
Simply clone the repository, build it with `cargo build --release` and run the `microbin` executable in the created `target/release/` directory. It will start listening on 0.0.0.0:8080. You can change the port or bind address with CL arguments `-p (--port)` or `-b (--bind)` respectively . For other arguments see [the Wiki](https://github.com/szabodanika/microbin/wiki).
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).
```
git clone https://github.com/szabodanika/microbin.git
@ -121,49 +111,6 @@ cargo build --release
./target/release/microbin -p 80
```
### Building Docker Image
MicroBin includes a Dockerfile. To build the image, follow these steps:
```
git clone https://github.com/szabodanika/microbin.git
cd microbin
docker build -t microbin-docker .
```
Then, for `docker compose` you can repurpose the following example in your compose file:
```
services:
paste:
image: microbin-docker
restart: always
ports:
- "80:8080"
volumes:
- ./microbin-data:/usr/local/bin/pasta_data
```
To pass command line arguments you must edit the Dockerfile and change the CMD line. In this example we add the syntax highlighting option and enable private pastas:
```
CMD ["microbin", "--highlightsyntax", "--private"]
```
You then need to rebuild the image and recreate your container.
**Note:** If you are getting the following error about domain name resolution:
```
warning: spurious network error (2 tries remaining): failed to resolve address for github.com: Temporary failure in name resolution; class=Net (12)
warning: spurious network error (1 tries remaining): failed to resolve address for github.com: Temporary failure in name resolution; class=Net (12)
```
You might need to run `docker build` with the `--network` option:
```
docker build --network host -t microbin-docker .
```
### MicroBin as a service
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. If you installed MicroBin from cargo, your executable will be in your cargo directory, e.g. `/Users/daniel/.cargo/bin/microbin`.
@ -293,13 +240,6 @@ Default value: 8080
Sets the port for the server will be listening on.
### -b, --bind [ADDRESS]
Default value: 0.0.0.0
Sets the bind address for the server will be listening on. Both ipv4 and ipv6 are supported.
### --private
Enables private pastas. Adds a new checkbox to make your pasta private, which then won't show up on the pastalist page. With the URL to your pasta, it will still be accessible.

View File

@ -1,19 +0,0 @@
# Security Policy
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ------- | ------------------ |
| 1.1.* | :white_check_mark: |
| < 1.1.0 | :x: |
## Reporting a Vulnerability
Security vulnerabilities can be reported directly to
the developer/maintainer at d@szab.eu.
Sensitive information may be GPG encrypted with my public key available at
https://szab.eu/assets/files/daniel-szabo-pub.asc.

BIN
git/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

View File

@ -1,4 +1,3 @@
use std::net::IpAddr;
use clap::Parser;
use lazy_static::lazy_static;
@ -9,54 +8,51 @@ lazy_static! {
#[derive(Parser, Debug, Clone)]
#[clap(author, version, about, long_about = None)]
pub struct Args {
#[clap(long, env="MICROBIN_AUTH_USERNAME")]
#[clap(long)]
pub auth_username: Option<String>,
#[clap(long, env="MICROBIN_AUTH_PASSWORD")]
#[clap(long)]
pub auth_password: Option<String>,
#[clap(long, env="MICROBIN_EDITABLE")]
#[clap(long)]
pub editable: bool,
#[clap(long, env="MICROBIN_FOOTER_TEXT")]
#[clap(long)]
pub footer_text: Option<String>,
#[clap(long, env="MICROBIN_HIDE_FOOTER")]
#[clap(long)]
pub hide_footer: bool,
#[clap(long, env="MICROBIN_HIDE_HEADER")]
#[clap(long)]
pub hide_header: bool,
#[clap(long, env="MICROBIN_HIDE_LOGO")]
#[clap(long)]
pub hide_logo: bool,
#[clap(long, env="MICROBIN_NO_LISTING")]
#[clap(long)]
pub no_listing: bool,
#[clap(long, env="MICROBIN_HIGHLIGHTSYNTAX")]
#[clap(long)]
pub highlightsyntax: bool,
#[clap(short, long, env="MICROBIN_PORT", default_value_t = 8080)]
pub port: u16,
#[clap(short, long, default_value_t = 8080)]
pub port: u32,
#[clap(short, long, env="MICROBIN_BIND", default_value_t = IpAddr::from([0, 0, 0, 0]))]
pub bind: IpAddr,
#[clap(long, env="MICROBIN_PRIVATE")]
#[clap(long)]
pub private: bool,
#[clap(long, env="MICROBIN_PURE_HTML")]
#[clap(long)]
pub pure_html: bool,
#[clap(long, env="MICROBIN_READONLY")]
#[clap(long)]
pub readonly: bool,
#[clap(long, env="MICROBIN_TITLE")]
#[clap(long)]
pub title: Option<String>,
#[clap(short, long, env="MICROBIN_THREADS", default_value_t = 1)]
#[clap(short, long, default_value_t = 1)]
pub threads: u8,
#[clap(long, env="MICROBIN_WIDE")]
#[clap(long)]
pub wide: bool,
}
}

View File

@ -1,14 +1,11 @@
use crate::dbio::save_to_file;
use crate::pasta::PastaFile;
use crate::util::animalnumbers::to_animal_names;
use crate::util::misc::is_valid_url;
use crate::{AppState, Pasta, ARGS};
use actix_multipart::Multipart;
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};
@ -49,7 +46,7 @@ pub async fn create(
let mut new_pasta = Pasta {
id: rand::thread_rng().gen::<u16>() as u64,
content: String::from("No Text Content"),
file: None,
file: String::from("no-file"),
extension: String::from(""),
private: false,
editable: false,
@ -106,41 +103,27 @@ pub async fn create(
continue;
}
"file" => {
let path = field.content_disposition().get_filename();
let content_disposition = field.content_disposition();
let path = match path {
let filename = match content_disposition.get_filename() {
Some("") => continue,
Some(p) => p,
Some(filename) => filename.replace(' ', "_").to_string(),
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()))
std::fs::create_dir_all(format!("./pasta_data/{}", &new_pasta.id_as_animals()))
.unwrap();
let filepath = format!(
"./pasta_data/public/{}/{}",
&new_pasta.id_as_animals(),
&file.name()
);
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??;
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");
}
_ => {}

View File

@ -2,12 +2,10 @@ use actix_web::{get, web, HttpResponse};
use crate::args::ARGS;
use crate::endpoints::errors::ErrorTemplate;
use crate::pasta::PastaFile;
use crate::util::animalnumbers::to_u64;
use crate::util::misc::remove_expired;
use crate::AppState;
use askama::Template;
use std::fs;
#[get("/remove/{id}")]
pub async fn remove(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
@ -21,22 +19,10 @@ pub async fn remove(data: web::Data<AppState>, id: web::Path<String>) -> HttpRes
let id = to_u64(&*id.into_inner()).unwrap_or(0);
remove_expired(&mut pastas);
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", "/pastalist"))
@ -44,8 +30,6 @@ pub async fn remove(data: web::Data<AppState>, id: web::Path<String>) -> HttpRes
}
}
remove_expired(&mut pastas);
HttpResponse::Ok()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())

View File

@ -1,23 +1,37 @@
use actix_web::dev::JsonBody::Body;
use actix_web::error::UrlencodedError::ContentType;
use actix_web::web::Path;
use actix_web::{get, web, HttpResponse};
use askama::Template;
use std::io::ErrorKind::NotFound;
use std::marker::PhantomData;
#[derive(Template)]
#[template(path = "water.css", escape = "none")]
struct WaterCSS<'a> {
_marker: PhantomData<&'a ()>,
}
#[get("/static/{resource}")]
pub async fn static_resources(resource_id: web::Path<String>) -> HttpResponse {
match resource_id.into_inner().as_str() {
"water.css" => HttpResponse::Ok().content_type("text/css").body(
WaterCSS {
_marker: Default::default(),
}
.render()
.unwrap(),
),
return match resource_id.into_inner().as_str() {
"water.css" => HttpResponse::Ok()
.content_type("text/css")
.body(include_bytes!("../../templates/static/water.css").to_vec()),
"icon.ico" => HttpResponse::Ok()
.content_type("image/x-icon")
.body(include_bytes!("../../templates/static/icon.ico").to_vec()),
"icon-16x16.png" => HttpResponse::Ok()
.content_type("image/x-icon")
.body(include_bytes!("../../templates/static/icon-16x16.png").to_vec()),
"icon-32x32.png" => HttpResponse::Ok()
.content_type("image/x-icon")
.body(include_bytes!("../../templates/static/icon-32x32.png").to_vec()),
"icon-192x192.png" => HttpResponse::Ok()
.content_type("image/x-icon")
.body(include_bytes!("../../templates/static/icon-192x192.png").to_vec()),
"icon-512x512.png" => HttpResponse::Ok()
.content_type("image/x-icon")
.body(include_bytes!("../../templates/static/icon-512x512.png").to_vec()),
"apple-touch-icon.png" => HttpResponse::Ok()
.content_type("image/x-icon")
.body(include_bytes!("../../templates/static/apple-touch-icon.png").to_vec()),
_ => HttpResponse::NotFound().content_type("text/html").finish(),
}
};
}

View File

@ -58,16 +58,15 @@ async fn main() -> std::io::Result<()> {
.init();
log::info!(
"MicroBin starting on http://{}:{}",
ARGS.bind.to_string(),
"MicroBin starting on http://127.0.0.1:{}",
ARGS.port.to_string()
);
match fs::create_dir_all("./pasta_data/public") {
match fs::create_dir_all("./pasta_data") {
Ok(dir) => dir,
Err(error) => {
log::error!("Couldn't create data directory ./pasta_data/public/: {:?}", error);
panic!("Couldn't create data directory ./pasta_data/public/: {:?}", error);
log::error!("Couldn't create data directory ./pasta_data: {:?}", error);
panic!("Couldn't create data directory ./pasta_data: {:?}", error);
}
};
@ -87,7 +86,7 @@ async fn main() -> std::io::Result<()> {
.service(edit::get_edit)
.service(edit::post_edit)
.service(static_resources::static_resources)
.service(actix_files::Files::new("/file", "./pasta_data/public/"))
.service(actix_files::Files::new("/file", "./pasta_data"))
.service(web::resource("/upload").route(web::post().to(create::create)))
.default_service(web::route().to(errors::not_found))
.wrap(middleware::Logger::default())
@ -98,7 +97,7 @@ async fn main() -> std::io::Result<()> {
HttpAuthentication::basic(util::auth::auth_validator),
))
})
.bind((ARGS.bind, ARGS.port))?
.bind(format!("0.0.0.0:{}", ARGS.port.to_string()))?
.workers(ARGS.threads as usize)
.run()
.await

View File

@ -1,39 +1,16 @@
use bytesize::ByteSize;
use chrono::{Datelike, Local, TimeZone, Timelike};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::Path;
use chrono::{DateTime, Datelike, NaiveDateTime, Timelike, Utc};
use serde::{Deserialize, Serialize};
use crate::util::animalnumbers::to_animal_names;
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
}
}
#[derive(Serialize, Deserialize)]
pub struct Pasta {
pub id: u64,
pub content: String,
pub file: Option<PastaFile>,
pub file: String,
pub extension: String,
pub private: bool,
pub editable: bool,
@ -48,9 +25,9 @@ impl Pasta {
}
pub fn created_as_string(&self) -> String {
let date = Local.timestamp(self.created, 0);
let date = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(self.created, 0), Utc);
format!(
"{:02}-{:02} {:02}:{:02}",
"{:02}-{:02} {}:{}",
date.month(),
date.day(),
date.hour(),
@ -62,9 +39,10 @@ impl Pasta {
if self.expiration == 0 {
String::from("Never")
} else {
let date = Local.timestamp(self.expiration, 0);
let date =
DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(self.expiration, 0), Utc);
format!(
"{:02}-{:02} {:02}:{:02}",
"{:02}-{:02} {}:{}",
date.month(),
date.day(),
date.hour(),

View File

@ -22,22 +22,20 @@ pub fn remove_expired(pastas: &mut Vec<Pasta>) {
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())
match fs::remove_file(format!("./pasta_data/{}/{}", p.id_as_animals(), p.file)) {
Ok(_) => {}
Err(_) => {
log::error!("Failed to delete file {}!", p.file)
}
}
// and remove the containing directory
match fs::remove_dir(format!("./pasta_data/{}/", p.id_as_animals())) {
Ok(_) => {}
Err(_) => {
log::error!("Failed to delete directory {}!", p.file)
}
}
// remove
false
}
});

View File

@ -1,12 +1,19 @@
<!DOCTYPE html>
<html>
<head>
{% if args.title.as_ref().is_none() %}
{% if args.footer_text.as_ref().is_none() %}
<title>MicroBin</title>
{%- else %}
<title>{{ args.title.as_ref().unwrap() }}</title>
{%- endif %}
<link rel="icon" type="image/png" href="/static/icon.ico">
<link rel="icon" type="image/png" href="/static/icon-16x16.png" sizes="16x16">
<link rel="icon" type="image/png" href="/static/icon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="/static/icon-192x192.png" sizes="192x192">
<link rel="icon" type="image/png" href="/static/icon-512x512.png">
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% if !args.pure_html %}
@ -39,11 +46,12 @@
<b style="margin-right: 0.5rem">
{% if !args.hide_logo %}
<i><span style="font-size:2.2rem; margin-right:1rem">μ</span></i>
<!-- <i><span style="font-size:2.2rem; margin-right:1rem">μ</span></i>-->
<img width="25" style="margin-bottom: -0.4rem; margin-right: 0.4rem" src="static/icon-192x192.png"/>
{%- endif %}
{% if args.title.as_ref().is_none() %}
MicroBin
{% if args.footer_text.as_ref().is_none() %}
<span>MicroBin</span>
{%- else %}
{{ args.title.as_ref().unwrap() }}
{%- endif %}

View File

@ -4,10 +4,10 @@
<div style="display: grid;
grid-gap: 4px;
grid-template-columns: repeat(auto-fit, 234px);
grid-template-rows: repeat(1, 100px); ">
grid-template-rows: repeat(1, 78px); ">
<div>
<label for="expiration">Expiration</label><br>
<select style="width: 100%;" name="expiration" id="expiration">
<select style="width: 100%;margin-top: 0" name="expiration" id="expiration">
<optgroup label="Expire">
<option value="1min">1 minute</option>
<option value="10min">10 minutes</option>
@ -21,7 +21,7 @@
{% if args.highlightsyntax %}
<div>
<label for="syntax-highlight">Syntax Highlighting</label><br>
<select style="width: 100%;" name="syntax-highlight" id="syntax-highlight">
<select style="width: 100%; ;margin-top: 0" name="syntax-highlight" id="syntax-highlight">
<option value="none">None</option>
<optgroup label="Source Code">
<option value="sh">Bash Shell</option>
@ -58,13 +58,12 @@
{%- else %}
<input type="hidden" name="syntax-highlight" value="none">
{%- endif %}
<div>
<label>File attachment</label>
<br>
<input style="width: 100%;" type="file" id="file" name="file">
<label>File attachment</label><br>
<input style="width: 100%; height: 21px" type="file" id="file" name="file">
</div>
</div>
<br>
<label>Content</label>
<br>
<textarea style="width: 100%; min-height: 100px" name="content" autofocus></textarea>

View File

@ -1,28 +1,19 @@
{% include "header.html" %}
<div style="float: left">
<a style="margin-right: 0.5rem" href="/raw/{{pasta.id_as_animals()}}">Raw Text Content</a>
{% if pasta.file.is_some() %}
<a style="margin-right: 0.5rem; margin-left: 0.5rem"
href="/file/{{pasta.id_as_animals()}}/{{pasta.file.as_ref().unwrap().name()}}">
Attached file'{{pasta.file.as_ref().unwrap().name()}}' [{{pasta.file.as_ref().unwrap().size}}]
</a>
{%- endif %}
{% if pasta.editable %}
<a style="margin-right: 0.5rem; margin-left: 0.5rem" href="/edit/{{pasta.id_as_animals()}}">Edit</a>
{%- endif %}
<a style="margin-right: 0.5rem; margin-left: 0.5rem" href="/remove/{{pasta.id_as_animals()}}">Remove</a>
</div>
<div style="float: right">
<a href="/pasta/{{pasta.id_as_animals()}}"><i>{{pasta.id_as_animals()}}</i></a>
</div>
<br>
<div style="clear: both;">
{% if args.highlightsyntax %}
<pre><code>{{pasta.content_syntax_highlighted()}}</code></pre>
{%- else %}
<pre><code>{{pasta.content_not_highlighted()}}</code></pre>
{%- endif %}
</div>
<a style="margin-right: 0.5rem" href="/raw/{{pasta.id_as_animals()}}">Raw Text Content</a>
{% if pasta.file != "no-file" %}
<a style="margin-right: 0.5rem; margin-left: 0.5rem" href="/file/{{pasta.id_as_animals()}}/{{pasta.file}}">Attached file
'{{pasta.file}}'</a>
{%- endif %}
{% if pasta.editable %}
<a style="margin-right: 0.5rem; margin-left: 0.5rem" href="/edit/{{pasta.id_as_animals()}}">Edit</a>
{%- endif %}
<a style="margin-right: 0.5rem; margin-left: 0.5rem" href="/remove/{{pasta.id_as_animals()}}">Remove</a>
{% if args.highlightsyntax %}
<pre><code>{{pasta.content_syntax_highlighted()}}</code></pre>
{%- else %}
<pre><code>{{pasta.content_not_highlighted()}}</code></pre>
{%- endif %}
<style>
code-line {
counter-increment: listing;

View File

@ -48,8 +48,8 @@
</td>
<td>
<a style="margin-right:1rem" href="/raw/{{pasta.id_as_animals()}}">Raw</a>
{% if pasta.file.is_some() %}
<a style="margin-right:1rem" href="/file/{{pasta.id_as_animals()}}/{{pasta.file.as_ref().unwrap().name()}}">File</a>
{% if pasta.file != "no-file" %}
<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="/edit/{{pasta.id_as_animals()}}">Edit</a>

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 875 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

BIN
templates/static/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB