Browse Source

first commit

master
forest 3 years ago
commit
60a432ba33
7 changed files with 682 additions and 0 deletions
  1. +1
    -0
      .gitignore
  2. +10
    -0
      Dockerfile
  3. +25
    -0
      ReadMe.md
  4. +52
    -0
      index.html
  5. +138
    -0
      main.go
  6. +175
    -0
      static/application.css
  7. +281
    -0
      static/application.js

+ 1
- 0
.gitignore View File

@ -0,0 +1 @@
picopublish

+ 10
- 0
Dockerfile View File

@ -0,0 +1,10 @@
FROM ubuntu
WORKDIR /picopublish
ADD * /picopublish/
ADD static/* /picopublish/static/
RUN chmod +x /picopublish/picopublish
ENTRYPOINT /picopublish/picopublish

+ 25
- 0
ReadMe.md View File

@ -0,0 +1,25 @@
## webclip
The easy way to get a file off of a remote machine without using `scp`.
Usage:
```
me@my-desktop:~$ ssh foo@fooserver.io
foo@fooserver:~$ cd /my/stuff
foo@fooserver:/my/stuff$ ls -lah
drwxrwxr-x 2 foo foo 4.0K Nov 6 18:48 .
drwxrwxr-x 3 foo foo 4.0K Nov 6 18:48 ..
-rw-rw-r-- 1 foo foo 10.0M Nov 6 18:48 myArchive.gz
-rw-rw-r-- 1 foo foo 28.0K Nov 6 18:48 myFile.txt
foo@fooserver:/my/stuff$ curl -s https://webclip.mydomain.com/myArchive.gz | bash
200 ok I got "myArchive.gz" with 10000000 bytes.
foo@fooserver:/my/stuff$ exit
me@my-desktop:~$ curl -s https://webclip.mydomain.com > myArchive.gz
```
After you clip something you may also grab it by hitting https://webclip.mydomain.com in your web browser.
Once it is downloaded the first time, it will be removed from the server and cannot be downloaded again.
Only one file can be clipped at once.

+ 52
- 0
index.html View File

@ -0,0 +1,52 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>pico publish</title>
<link rel="stylesheet" type="text/css" href="static/application.css">
</head>
<body>
<div class="header">
<h3>Sequential Read Pico Publish</h3>
<div style="float:right; padding-right:20px;">
<a href="https://git.sequentialread.com/forest/pico-publish">source code</a>
</div>
</div>
<div class="header-shadow"></div>
<div class="splash content">
<h3>Publish a File:</h3>
<div>
<label for="file">File</label>
<input type="file" id="file"></input>
</div>
<div>
<label for="filename">Filename (optional)</label>
<input id="filename" type="text"></input>
</div>
<div>
<label for="content-type">Content-Type (optional)</label>
<input id="content-type" type="text"></input>
</div>
<div>
<button id="upload-button" style="float:right;">
Publish
</button>
</div>
<br/><br/>
</div>
<div class="modal-container" id="modal-container" style="display:none;">
<div class="modal content" >
<h3 id="modal-title"></h3>
<div id="modal-body"></div>
<div id="modal-footer" style="height:50px;"></div>
</div>
</div>
<div class="modal-container" id="progress-container" style="display:none;">
<div class="loader">loading</div>
</div>
<script src="static/application.js"></script>
</body>
</html>

+ 138
- 0
main.go View File

@ -0,0 +1,138 @@
package main
import (
"fmt"
"io/ioutil"
"net/http"
"strings"
"io"
"os"
"bytes"
"path/filepath"
)
var secretPassword = ""
var dataPath = ""
func indexHtml(response http.ResponseWriter, request *http.Request) {
if request.URL.Path != "/" {
response.WriteHeader(404)
fmt.Fprintf(response, "404 not found: %s", request.URL.Path)
return
}
if request.Method == "GET" {
buffer, err := ioutil.ReadFile("index.html")
if err != nil {
response.WriteHeader(500)
fmt.Print("500 index.html is missing")
fmt.Fprint(response, "500 index.html is missing")
return
}
io.Copy(response, bytes.NewBuffer(buffer))
} else {
response.Header().Add("Allow", "GET")
response.WriteHeader(405)
fmt.Fprint(response, "405 Method Not Supported")
}
}
func files(response http.ResponseWriter, request *http.Request) {
var filename string
var pathElements = strings.Split(request.RequestURI, "/")
filename = pathElements[len(pathElements)-1]
if strings.Contains(filename, "/") || strings.Contains(filename, "\\") {
response.WriteHeader(404)
fmt.Print("illegal file name: " + filename + "\n\n")
fmt.Fprint(response, "illegal file name.")
return
}
fullFilePath := filepath.Join(dataPath, filename)
contentTypeFilePath := fullFilePath + ".content-type"
if request.Method == "GET" {
_, err := os.Stat(fullFilePath)
if err != nil {
response.WriteHeader(404)
fmt.Print("404 file not found: " + fullFilePath + "\n\n")
fmt.Fprint(response, "404 file not found")
return
}
file, err := os.Open(fullFilePath)
if err != nil {
response.WriteHeader(500)
fmt.Printf("500 error opening file: " + fullFilePath + " %s \n\n", err)
fmt.Fprint(response, "500 error opening file")
return
}
defer file.Close()
contentTypeBytes, err := ioutil.ReadFile(contentTypeFilePath)
contentType := "text/plain"
if err == nil && string(contentTypeBytes) != "" {
contentType = string(contentTypeBytes)
}
response.Header().Add("Content-Type", contentType)
io.Copy(response, file)
} else if request.Method == "POST" {
_, requestPassword, _ := request.BasicAuth()
if secretPassword != "" && requestPassword != secretPassword {
http.Error(response, "Unauthorized.", 401)
} else {
_, err := os.Stat(fullFilePath)
if err == nil {
response.WriteHeader(400)
fmt.Print("400 bad request: " + fullFilePath + " already exists. \n\n")
fmt.Fprint(response, "400 bad request: a file named \"" + filename + "\" already exists.")
return
}
file, err := os.Create(fullFilePath)
if err != nil {
response.WriteHeader(500)
fmt.Printf("500 error opening file: " + fullFilePath + " %s \n\n", err)
fmt.Fprint(response, "500 error opening file")
return
}
defer file.Close()
if request.Header.Get("Content-Type") != "" {
err = ioutil.WriteFile(contentTypeFilePath, []byte(request.Header.Get("Content-Type")), 0644)
if err != nil {
response.WriteHeader(500)
fmt.Fprintf(response, "500 %s", err)
return
}
}
io.Copy(file, request.Body)
}
} else if request.Method == "DELETE" {
response.WriteHeader(500)
fmt.Fprint(response, "500 not implemented yet")
} else {
response.Header().Add("Allow", "GET, POST, DELETE")
response.WriteHeader(405)
fmt.Fprint(response, "405 Method Not Supported")
}
}
func main() {
secretPassword = os.ExpandEnv("$PICO_PUBLISH_PASSWORD")
dataPath = filepath.Join(".", "data")
os.MkdirAll(dataPath, os.ModePerm)
http.HandleFunc("/files/", files)
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
http.HandleFunc("/", indexHtml)
http.ListenAndServe(":8080", nil)
}

+ 175
- 0
static/application.css View File

@ -0,0 +1,175 @@
body {
background: #e4e4e4;
color: #333333;
padding: 0;
margin:0;
font-family: 'Ubuntu','Roboto','Open Sans',sans-serif;
}
a {
color: #9359fa;
}
a:visited {
color: #9359fa;
}
h3 {
margin-top:0px;
margin-bottom: 15px;
color: #333333;
}
input, button {
border-radius: 2px;
border: 1px solid #dddddd;
padding: 5px;
margin:5px;
}
input::-moz-focus-inner,button::-moz-focus-inner {
border: 0;
}
button:focus,input:focus {
border: 1px solid #bbaaff;
outline: none;
}
button{
background: #f4f4f4;
color: #333333;
}
button[disabled],button[disabled]:hover {
background: #ffffff;
color: #aaaaaa;
}
button:hover {
background: #ffffff;
}
button:active {
background: #ffffff;
border: 1px solid #dddddd;
}
input[type="text"]{
color: #666666;
}
textarea {
width: calc(100% - 20px);
height: 200px;
resize: vertical;
padding: 10px;
}
u {
font-weight: bold;
color: red;
}
.header {
color: white;
background: #9359fa;
border-bottom: 1px solid #452775;
height: 20px;
padding:12px;
padding-top:6px;
}
.header h3 {
color: white;
font-size:18px;
font-weight: bold;
display: inline-block;
padding: 0;
margin: 0;
}
.header a {
color: white;
}
.header a:visited {
color: white;
}
.header-shadow {
height: 3px;
border-top: 1px solid #727630;
background: #f6ff72;
border-bottom: 1px solid #bbbbbb;
margin-bottom: 20px;
}
.content {
background: white;
color: #333333;
border-radius: 5px;
border: 1px solid #dddddd;
margin: auto;
padding: 20px;
padding-top:15px;
}
.content.splash {
max-width: 800px;
}
.modal-container {
position: fixed;
top:0;
left:0;
width:100%;
height:100%;
background: rgba(230,230,230,0.7);
z-index: 100;
}
.modal {
margin-top: 100px;
max-width: 500px;
}
.loader,
.loader:before,
.loader:after {
background: rgba(0,0,0,0.4);
-webkit-animation: load1 1s infinite ease-in-out;
animation: load1 1s infinite ease-in-out;
width: 1em;
height: 4em;
}
.loader:before,
.loader:after {
position: absolute;
top: 0;
content: '';
}
.loader:before {
left: -1.5em;
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
.loader {
color: rgba(0,0,0,0.4);
text-indent: -9999em;
margin: 160px auto;
position: relative;
font-size: 11px;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
.loader:after {
left: 1.5em;
}
@-webkit-keyframes load1 {
0%,
80%,
100% {
box-shadow: 0 0;
height: 4em;
}
40% {
box-shadow: 0 -2em;
height: 5em;
}
}
@keyframes load1 {
0%,
80%,
100% {
box-shadow: 0 0;
height: 4em;
}
40% {
box-shadow: 0 -2em;
height: 5em;
}
}

+ 281
- 0
static/application.js View File

@ -0,0 +1,281 @@
'use strict';
window.picoPublish = {};
(function(app, document, window, undefined){
app.modalService = new (function ModalService() {
var modalIsOpen = false;
var enterKeyAction;
var escapeKeyAction;
var KEYCODE_ESCAPE = 27;
var KEYCODE_ENTER = 13;
window.addEventListener("keydown", (event) => {
if(event.keyCode == KEYCODE_ENTER && enterKeyAction) {
enterKeyAction();
}
if(event.keyCode == KEYCODE_ESCAPE && escapeKeyAction) {
escapeKeyAction();
}
}, false);
this.open = (title, body, controller, buttons) => {
return new Promise((resolve, reject) => {
modalIsOpen = true;
document.getElementById('modal-container').style.display = 'block';
document.getElementById('modal-title').innerHTML = title;
document.getElementById('modal-body').innerHTML = body;
var footer = document.getElementById('modal-footer');
var closeModal = () => {
modalIsOpen = false;
enterKeyAction = null;
escapeKeyAction = null;
document.getElementById('modal-container').style.display = 'none';
footer.innerHTML = '';
};
var buttonResolve = (arg) => {
closeModal();
resolve(arg);
};
var buttonReject = (arg) => {
closeModal();
reject(arg);
};
buttons.reverse();
buttons.forEach(button => {
var buttonElement = document.createElement("button");
if(button.id) {
buttonElement.id = button.id;
}
buttonElement.style.float = "right";
buttonElement.innerHTML = button.innerHTML;
var clickAction = () => {
if(!buttonElement.disabled) {
button.onclick(buttonResolve, buttonReject);
}
};
buttonElement.onclick = clickAction;
if(button.enterKey) {
enterKeyAction = clickAction;
}
if(button.escapeKey) {
escapeKeyAction = clickAction;
}
footer.appendChild(buttonElement);
});
controller(buttonResolve, buttonReject);
});
};
})();
})(window.picoPublish, document, window);
(function(app, window, document, undefined){
app.filePoster = new (function FilePoster(modalService) {
var baseUrl = "/files";
var requestsCurrentlyInFlight = 0;
var request = (method, url, headers, file) =>
new Promise((resolve, reject) => {
requestsCurrentlyInFlight += 1;
document.getElementById('progress-container').style.display = 'block';
var resolveAndPopInFlight = (result) => {
requestsCurrentlyInFlight -= 1;
if(requestsCurrentlyInFlight == 0) {
document.getElementById('progress-container').style.display = 'none';
}
resolve(result);
};
var rejectAndPopInFlight = (result) => {
requestsCurrentlyInFlight -= 1;
if(requestsCurrentlyInFlight == 0) {
document.getElementById('progress-container').style.display = 'none';
}
reject(result);
};
headers = headers || {};
var httpRequest = new XMLHttpRequest();
httpRequest.onloadend = () => {
if (httpRequest.status === 200) {
if(httpRequest.responseText.length == 0) {
resolveAndPopInFlight("");
} else {
resolveAndPopInFlight(httpRequest.responseText);
}
} else {
rejectAndPopInFlight(httpRequest.responseText);
}
};
//httpRequest.onerror = () => {
// console.log(`httpRequest.onerror: ${httpRequest.status} ${url}`);
// resolveAndPopInFlight(new RequestFailure(httpRequest, false));
//};
httpRequest.ontimeout = () => {
//console.log(`httpRequest.ontimeout: ${httpRequest.status} ${url}`);
rejectAndPopInFlight('HTTP Request timed out.');
};
httpRequest.open(method, url);
httpRequest.timeout = 2000;
Object.keys(headers)
.filter(key => key.toLowerCase() != 'host' && key.toLowerCase() != 'content-length')
.forEach(key => httpRequest.setRequestHeader(key, headers[key]));
if(file) {
var fileReader = new FileReader();
fileReader.readAsArrayBuffer(file);
fileReader.onload = function(e) {
httpRequest.send(e.target.result);
}
} else {
httpRequest.send();
}
});
this.post = (fileName, headers, file) => {
return request('POST', `${baseUrl}/${fileName}`, headers, file)
};
})(app.modalService);
})(window.picoPublish, window, document);
(function(app, window, undefined){
app.errorHandler = new (function ErrorHandler(modalService) {
this.errorContent = "";
this.onError = (message, fileName, lineNumber, column, err) => {
this.errorContent += `<p>${message || err.message} at ${fileName || ""}:${lineNumber || ""}</p>`;
console.log(message, fileName, lineNumber, column, err);
document.getElementById('progress-container').style.display = 'none';
modalService.open(
"JavaScript Error",
`
${this.errorContent}
`,
(resolve, reject) => {},
[{
innerHTML: "Ok",
enterKey: true,
escapeKey: true,
onclick: (resolve, reject) => resolve()
}]
).then(() => {
this.errorContent = "";
});
};
window.onerror = this.onError;
window.addEventListener("unhandledrejection", (unhandledPromiseRejectionEvent, promise) => {
var err = unhandledPromiseRejectionEvent.reason;
if(typeof err == "string") {
err = new Error(err);
}
if(err) {
this.onError(err.message, err.fileName, err.lineNumber, null, err);
}
});
})(app.modalService);
})(window.picoPublish, window);
(function(app, window, document, undefined){
app.mainController = new (function MainContrller(modalService, filePoster) {
document.getElementById("upload-button").addEventListener('click', () => {
var fileInput = document.getElementById("file");
var filenameInput = document.getElementById("filename");
var contentTypeInput = document.getElementById("content-type");
if(fileInput.files.length == 0 || fileInput.files.length > 1) {
modalService.open(
"Unsupported Operation",
"Zero files selected or more than one file selected. This is not supported.",
(resolve, reject) => {},
[{
innerHTML: "Ok",
enterKey: true,
escapeKey: true,
onclick: (resolve, reject) => resolve()
}]
);
} else {
if(filenameInput.value.replace(' ', '').length < 3 || filenameInput.value.indexOf('/') > -1 || filenameInput.value.indexOf('\\') > -1 ) {
filenameInput.value = fileInput.files[0].name;
}
if(contentTypeInput.value.replace(' ', '').length < 3 ) {
contentTypeInput.value = fileInput.files[0].type;
}
modalService.open(
"Password",
`
<input type="password" id="password"></input>
`,
(resolve, reject) => {
document.getElementById("password").focus();
},
[{
innerHTML: "Cancel",
escapeKey: true,
onclick: (resolve, reject) => reject()
},
{
innerHTML: "Ok",
enterKey: true,
onclick: (resolve, reject) => {
resolve(document.getElementById("password").value);
}
}]
).then((password) => {
filePoster.post(filenameInput.value, {'Content-Type': contentTypeInput.value, 'Authorization': `Basic ${btoa(`admin:${password}`)}`}, fileInput.files[0])
.then((responseText) => {
modalService.open(
"Success",
`<a href="${window.location}files/${filenameInput.value}">${window.location}files/${filenameInput.value}</a>`,
(resolve, reject) => {},
[{
innerHTML: "Ok",
enterKey: true,
onclick: (resolve, reject) => resolve()
}]
);
},
(responseText) => {
modalService.open(
"Failure",
responseText,
(resolve, reject) => {},
[{
innerHTML: "Ok",
enterKey: true,
onclick: (resolve, reject) => resolve()
}]
);
},
)
});
}
});
})(app.modalService, app.filePoster);
})(window.picoPublish, window, document);

Loading…
Cancel
Save