Skip to content

Commit 77ddd7e

Browse files
authored
Merge pull request #1546 from rktjmp/feat-pastebin
Add minimal pastebin behaviour
2 parents 6fa051c + ab6e289 commit 77ddd7e

File tree

5 files changed

+267
-85
lines changed

5 files changed

+267
-85
lines changed

data/style.scss

Lines changed: 132 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -551,84 +551,151 @@ th span.active span {
551551
background: var(--back_button_background_hover);
552552
}
553553

554+
//
555+
// Toolbar & tools inside the bar.
556+
//
557+
554558
.toolbar {
555-
display: flex;
556-
justify-content: space-between;
557-
flex-wrap: wrap;
559+
--tool-gap-between: 0.5rem;
560+
--tool-spacing-inside: 0.5rem;
558561
}
559562

560-
.download {
563+
.toolbar .tool_row {
561564
margin-top: 1rem;
562-
padding: 0.125rem;
563565
display: flex;
564-
flex-direction: row;
565-
align-items: flex-start;
566-
flex-wrap: wrap;
566+
gap: var(--tool-gap-between);
567+
flex-direction: column;
568+
@media (min-width: 760px) {
569+
flex-direction: row;
570+
}
567571
}
568572

569-
.download a,
570-
.download a:visited {
571-
color: var(--download_button_link_color);
572-
}
573+
// Upload tools has 4 configurations
574+
//
575+
// a) Upload enabled,
576+
// b) Upload enabled, mkdir enabled
577+
// c) Upload enabled, paste enabled
578+
// d) Upload enabled, mkdir enabled, paste enabled
579+
//
580+
// At larger screen sizes, for a and b, we render the tools horizontal, as at
581+
// min-content width. For c and d, we render upload (and mkdir) at min-content
582+
// width, stacked vertically and let paste fill the remaining space.
583+
//
584+
// At smaller screen sizes we render any available elememnts in a full-width
585+
// stack.
586+
//
587+
// We render via grid not flex as it affords us better control of the
588+
// stack/unstack in b.
589+
590+
@media (min-width: 760px) {
591+
.toolbar .tool_row.upload_tools {
592+
display: grid;
593+
grid-template-columns: min-content min-content;
594+
595+
.tool[data-tool="upload"] {
596+
grid-column: 1 / 2;
597+
grid-row: 1 / 2;
598+
}
573599

574-
.download a {
575-
background: var(--download_button_background);
576-
padding: 0.5rem;
577-
border-radius: 0.2rem;
578-
}
600+
.tool[data-tool="mkdir"] {
601+
grid-column: 2 / 3;
602+
grid-row: 1 / 2;
603+
}
579604

580-
.download a:hover {
581-
background: var(--download_button_background_hover);
582-
color: var(--download_button_link_color_hover);
605+
&:has([data-tool="pastebin"]) {
606+
grid-template-columns: min-content auto;
607+
.tool[data-tool="upload"] {
608+
grid-column: 1 / 2;
609+
grid-row: 1 / 2;
610+
}
611+
.tool[data-tool="mkdir"] {
612+
grid-column: 1 / 2;
613+
grid-row: 2 / 3;
614+
}
615+
.tool[data-tool="pastebin"] {
616+
grid-column: 2 / 3;
617+
grid-row: 1 / 3;
618+
}
619+
}
620+
}
583621
}
584622

585-
.download a:not(:last-of-type) {
586-
margin-right: 1rem;
623+
.toolbar form.tool {
624+
padding: 1rem;
625+
border: 1px solid var(--upload_form_border_color);
626+
background: var(--upload_form_background);
627+
& > * {
628+
margin-bottom: var(--tool-spacing-inside);
629+
&:last-child {
630+
margin-bottom: 0;
631+
}
632+
}
633+
p {
634+
font-size: 0.8rem;
635+
color: var(--upload_text_color);
636+
}
637+
input {
638+
padding: 0.5rem;
639+
margin-right: 0.2rem;
640+
border-radius: 0.2rem;
641+
border: 0;
642+
display: inline;
643+
}
644+
button {
645+
background: var(--upload_button_background);
646+
padding: 0.5rem;
647+
border-radius: 0.2rem;
648+
color: var(--upload_button_text_color);
649+
border: none;
650+
min-width: max-content;
651+
}
652+
div {
653+
display: flex;
654+
align-items: baseline;
655+
justify-content: space-between;
656+
}
587657
}
588658

589-
.toolbar_box_group {
590-
min-width: max-content;
591-
}
659+
//
660+
// Toolbar tool specific styling
661+
//
592662

593-
.toolbar_box {
594-
margin-top: 1rem;
663+
.toolbar .tool[data-tool="download"] {
664+
padding: 0.125rem;
595665
display: flex;
596-
justify-content: flex-end;
597-
}
666+
flex-direction: row;
667+
align-items: flex-start;
668+
flex-wrap: wrap;
598669

599-
.toolbar_box p {
600-
font-size: 0.8rem;
601-
margin-bottom: 1rem;
602-
color: var(--upload_text_color);
603-
}
670+
a {
671+
background: var(--download_button_background);
672+
padding: 0.5rem;
673+
border-radius: 0.2rem;
674+
}
604675

605-
.toolbar_box form {
606-
padding: 1rem;
607-
border: 1px solid var(--upload_form_border_color);
608-
background: var(--upload_form_background);
609-
}
676+
a, a:visited {
677+
color: var(--download_button_link_color);
678+
}
610679

611-
.toolbar_box input {
612-
padding: 0.5rem;
613-
margin-right: 0.2rem;
614-
border-radius: 0.2rem;
615-
border: 0;
616-
display: inline;
617-
}
680+
a:hover {
681+
background: var(--download_button_background_hover);
682+
color: var(--download_button_link_color_hover);
683+
}
618684

619-
.toolbar_box button {
620-
background: var(--upload_button_background);
621-
padding: 0.5rem;
622-
border-radius: 0.2rem;
623-
color: var(--upload_button_text_color);
624-
border: none;
625-
min-width: max-content;
685+
a:not(:last-of-type) {
686+
margin-right: 1rem;
687+
}
626688
}
627689

628-
.toolbar_box div {
629-
display: flex;
630-
align-items: baseline;
631-
justify-content: space-between;
690+
.toolbar .tool[data-tool="pastebin"] {
691+
textarea {
692+
width: 100%;
693+
resize: vertical;
694+
min-height: 4rem;
695+
padding: 0.5rem;
696+
border-radius: 0.2rem;
697+
border: 0;
698+
}
632699
}
633700

634701
.form,
@@ -676,13 +743,6 @@ th span.active span {
676743
margin-top: 4rem;
677744
}
678745

679-
@media (min-width: 900px) {
680-
.toolbar_box_group {
681-
display: flex;
682-
justify-content: flex-end;
683-
}
684-
}
685-
686746
@media (max-width: 760px) {
687747
nav {
688748
padding: 0 2.5rem;
@@ -768,6 +828,14 @@ th span.active span {
768828
h1 {
769829
font-size: 1.375em;
770830
}
831+
832+
nav {
833+
padding: 0 1rem;
834+
}
835+
836+
.container {
837+
padding: 1rem;
838+
}
771839
}
772840

773841
@media (max-width: 400px) {

src/args.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,17 @@ pub struct CliArgs {
265265
)]
266266
pub mkdir_enabled: bool,
267267

268+
/// Enable creating pastebin 'pastes'
269+
///
270+
/// 'pastes' are plaintext files created in the current directory. Creation requires file
271+
/// uploads be enabled.
272+
#[arg(
273+
long = "pastebin",
274+
requires = "allowed_upload_dir",
275+
env = "MINISERVE_PASTEBIN_ENABLED"
276+
)]
277+
pub pastebin_enabled: bool,
278+
268279
/// Specify uploadable media types
269280
#[arg(
270281
short = 'm',

src/config.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ pub struct MiniserveConfig {
118118
/// Enable file upload
119119
pub file_upload: bool,
120120

121+
/// Enable pastepin creation
122+
pub pastebin_enabled: bool,
123+
121124
/// Max amount of concurrency when uploading multiple files
122125
pub web_upload_concurrency: usize,
123126

@@ -349,6 +352,7 @@ impl MiniserveConfig {
349352
directory_size: args.directory_size,
350353
mkdir_enabled: args.mkdir_enabled,
351354
file_upload: args.allowed_upload_dir.is_some(),
355+
pastebin_enabled: args.pastebin_enabled,
352356
web_upload_concurrency: args.web_upload_concurrency,
353357
#[cfg(unix)]
354358
upload_chmod,

src/renderer.rs

Lines changed: 80 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -106,37 +106,48 @@ pub fn page(
106106
}
107107
div.toolbar {
108108
@if conf.tar_enabled || conf.tar_gz_enabled || conf.zip_enabled {
109-
div.download {
110-
@for archive_method in ArchiveMethod::iter() {
111-
@if archive_method.is_enabled(conf.tar_enabled, conf.tar_gz_enabled, conf.zip_enabled) {
112-
(archive_button(archive_method, sort_method, sort_order))
109+
div.tool_row.download_tools {
110+
div.tool data-tool="download" {
111+
@for archive_method in ArchiveMethod::iter() {
112+
@if archive_method.is_enabled(conf.tar_enabled, conf.tar_gz_enabled, conf.zip_enabled) {
113+
(archive_button(archive_method, sort_method, sort_order))
114+
}
113115
}
114116
}
115117
}
116118
}
117-
div.toolbar_box_group {
119+
120+
div.tool_row.upload_tools {
118121
@if conf.file_upload && upload_allowed {
119-
div.toolbar_box {
120-
form id="file_submit" action=(upload_action) method="POST" enctype="multipart/form-data" {
121-
p { "Select a file to upload or drag it anywhere into the window" }
122-
div {
123-
@match &conf.uploadable_media_type {
124-
Some(accept) => {input #file-input accept=(accept) type="file" name="file_to_upload" required="" multiple {}},
125-
None => {input #file-input type="file" name="file_to_upload" required="" multiple {}}
126-
}
127-
button type="submit" { "Upload file" }
122+
form.tool id="file_submit" data-tool="upload" action=(upload_action) method="POST" enctype="multipart/form-data" {
123+
p { "Select a file to upload or drag it anywhere into the window" }
124+
div {
125+
@match &conf.uploadable_media_type {
126+
Some(accept) => {input #file-input accept=(accept) type="file" name="file_to_upload" required="" multiple {}},
127+
None => {input #file-input type="file" name="file_to_upload" required="" multiple {}}
128128
}
129+
button type="submit" title="Upload File" { "Upload file" }
129130
}
130131
}
131132
}
132133
@if conf.mkdir_enabled && upload_allowed {
133-
div.toolbar_box {
134-
form id="mkdir" action=(mkdir_action) method="POST" enctype="multipart/form-data" {
135-
p { "Specify a directory name to create" }
136-
div.toolbar_box {
137-
input type="text" name="mkdir" required="" placeholder="Directory name" {}
138-
button type="submit" { "Create directory" }
139-
}
134+
form.tool id="mkdir" data-tool="mkdir" action=(mkdir_action) method="POST" enctype="multipart/form-data" {
135+
p { "Specify a directory name to create" }
136+
div {
137+
input type="text" name="mkdir" required="" placeholder="Directory name" {}
138+
button type="submit" title="Create directory" { "Create directory" }
139+
}
140+
}
141+
}
142+
@if conf.pastebin_enabled && upload_allowed {
143+
form.tool id="pastebin" data-tool="pastebin" {
144+
p { "Create a text file in the current directory, a random filename will be generated, or you may specify one." }
145+
div {
146+
textarea #pastebin_content name="paste_content" title="Text content" required="" { }
147+
}
148+
div {
149+
input type="text" name="paste_filename" title="Filename" placeholder="Filename (Optional)" autocomplete="off" {}
150+
button type="submit" title="Create file" { "Create file" }
140151
}
141152
}
142153
}
@@ -1079,6 +1090,54 @@ fn page_header(
10791090
})
10801091
}
10811092
}
1093+
1094+
// Bind pastebin submission to create a text/plain blob which is injected
1095+
// into the upload input then submitted. A title is automatically generated
1096+
// if none is given.
1097+
const fileUploadForm = document.querySelector('#file_submit');
1098+
const fileUploadInput = document.querySelector('#file_submit input[type=file]');
1099+
const pastebinForm = document.querySelector('form#pastebin');
1100+
const pastebinFilename = pastebinForm.querySelector('input[name=paste_filename]');
1101+
const pastebinContent = pastebinForm.querySelector('textarea');
1102+
pastebinContent.addEventListener('keydown', (event) => {
1103+
// common convenience of ctrl-enter to submit
1104+
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
1105+
event.preventDefault();
1106+
event.target.form.requestSubmit();
1107+
}
1108+
});
1109+
1110+
pastebinForm.addEventListener('submit', (event) => {
1111+
// The pastebin form is "dead" and should not cause any page-submit
1112+
// events. We capture the pastebin form content, convert it into a
1113+
// in-memory blob, then pass that blob to the regular fileUpload form
1114+
// for submission, as if a user and selected a real file.
1115+
event.preventDefault();
1116+
const text = pastebinContent.value;
1117+
const title = ((inputValue) => {
1118+
const title = inputValue.trim();
1119+
if (title.length === 0) {
1120+
const suffix = crypto.randomUUID().substring(0,6);
1121+
return `paste-${suffix}.txt`;
1122+
} else {
1123+
// use given extension if one is present, otherwise make it
1124+
// .txt. We're quite liberal in what we consider an extension,
1125+
// any number of alpha-numeric after a dot.
1126+
if (/\.[0-9a-z]+$/i.test(title)) {
1127+
return title;
1128+
} else {
1129+
return `${title}.txt`;
1130+
}
1131+
}
1132+
})(pastebinFilename.value);
1133+
// Package text as a file and submit
1134+
const blob = new Blob([text], {type: 'text/plain'});
1135+
const file = new File([blob], title, {type: 'text/plain'});
1136+
const container = new DataTransfer();
1137+
container.items.add(file);
1138+
fileUploadInput.files = container.files;
1139+
fileUploadForm.submit();
1140+
});
10821141
}
10831142
"#))
10841143
}

0 commit comments

Comments
 (0)