Compare commits
36 Commits
v1.1.0-rc1
...
v1.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 396672b02b | |||
| d25b867e90 | |||
| cd60e4c855 | |||
| ee72f188a2 | |||
| 30f5b40e1d | |||
| 3a103ae484 | |||
| 08321818ff | |||
| 14456e6539 | |||
| f27d14aac4 | |||
| 6ab2b08099 | |||
| 930869759b | |||
| d35c81ed21 | |||
| 139117b4d5 | |||
| b88273ec64 | |||
| 79cb8733e5 | |||
| 366c307e02 | |||
| 23ad509f33 | |||
| a6e495f74d | |||
| b3ec40ae65 | |||
| 87f0cb016d | |||
| 381e67bb39 | |||
| c2f4de822f | |||
| ffd4c03d9c | |||
| 2e08ac06ca | |||
| 6d739972de | |||
| e36fd43f9b | |||
| 35bbc6c61b | |||
| 987c704730 | |||
| 9669f2aa0b | |||
| 99139e360d | |||
| 696a92096d | |||
| f125c23fb9 | |||
| c50d6c4bfe | |||
| 5edea74333 | |||
| 1882a94e65 | |||
| 1309293705 |
9
.hgtags
9
.hgtags
@@ -1,2 +1,11 @@
|
||||
e2250a7fd29052ea767f18e1459cabea4cd7efd3 release-1.0.0
|
||||
b8975b9e75648a7c2a5003c67db92cf2216e01c0 release-1.0.1
|
||||
c7b699105bd166e7a01aa0e678d34624680bf81e release-1.1.0
|
||||
c7b699105bd166e7a01aa0e678d34624680bf81e release-1.1.0
|
||||
0000000000000000000000000000000000000000 release-1.1.0
|
||||
0000000000000000000000000000000000000000 release-1.1.0
|
||||
cfb1e028fd0627614aa01184893f9f29f20a347e release-1.1.0
|
||||
cfb1e028fd0627614aa01184893f9f29f20a347e release-1.1.0
|
||||
0000000000000000000000000000000000000000 release-1.1.0
|
||||
0000000000000000000000000000000000000000 release-1.1.0
|
||||
98da2ebf0d50dffe8b625457a639bc2f15519714 release-1.1.0
|
||||
|
||||
2
Makefile
2
Makefile
@@ -2,7 +2,7 @@
|
||||
# Makefile for contented
|
||||
#
|
||||
|
||||
VERSION:=1.1.0
|
||||
VERSION:=1.2.0
|
||||
|
||||
SOURCES:=Makefile \
|
||||
static \
|
||||
|
||||
31
Server.go
31
Server.go
@@ -5,6 +5,8 @@ import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -20,14 +22,23 @@ type ServerPublicProperties struct {
|
||||
}
|
||||
|
||||
type ServerOptions struct {
|
||||
DataDirectory string
|
||||
DBPath string
|
||||
BandwidthLimit int64
|
||||
TrustXForwardedFor bool
|
||||
EnableHomepage bool
|
||||
DataDirectory string
|
||||
DBPath string
|
||||
DiskFilesWorldReadable bool
|
||||
BandwidthLimit int64
|
||||
TrustXForwardedFor bool
|
||||
EnableHomepage bool
|
||||
ServerPublicProperties
|
||||
}
|
||||
|
||||
func (this *ServerOptions) FileMode() os.FileMode {
|
||||
if this.DiskFilesWorldReadable {
|
||||
return 0644
|
||||
} else {
|
||||
return 0600
|
||||
}
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
opts ServerOptions
|
||||
db *bolt.DB
|
||||
@@ -90,8 +101,11 @@ func (this *Server) remoteIP(r *http.Request) string {
|
||||
const (
|
||||
downloadUrlPrefix = `/get/`
|
||||
metadataUrlPrefix = `/info/`
|
||||
previewUrlPrefix = `/p/`
|
||||
)
|
||||
|
||||
var rxThumbUrl = regexp.MustCompile(`^/thumb/(.)/(.*)$`)
|
||||
|
||||
func (this *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set(`Server`, SERVER_HEADER)
|
||||
w.Header().Set(`Access-Control-Allow-Origin`, `*`) // Blanket allow CORS
|
||||
@@ -110,6 +124,13 @@ func (this *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
} else if r.Method == "GET" && strings.HasPrefix(r.URL.Path, metadataUrlPrefix) {
|
||||
this.handleInformation(w, r.URL.Path[len(metadataUrlPrefix):])
|
||||
|
||||
} else if r.Method == "GET" && strings.HasPrefix(r.URL.Path, previewUrlPrefix) {
|
||||
this.handlePreview(w, r.URL.Path[len(previewUrlPrefix):])
|
||||
|
||||
} else if r.Method == "GET" && rxThumbUrl.MatchString(r.URL.Path) {
|
||||
parts := rxThumbUrl.FindStringSubmatch(r.URL.Path)
|
||||
this.handleThumb(w, r, parts[1][0], parts[2])
|
||||
|
||||
} else if r.Method == "GET" && r.URL.Path == `/about` {
|
||||
this.handleAbout(w)
|
||||
|
||||
|
||||
9
TODO.txt
9
TODO.txt
@@ -6,8 +6,9 @@ TODO
|
||||
|
||||
- Encrypted at rest (anti- provider snooping)
|
||||
|
||||
- Nicer preview page after uploading
|
||||
|
||||
- Gallery preview (multiple element hashid)
|
||||
|
||||
- Prevent selecting around the toggle buttons (firefox for android)
|
||||
|
||||
- Detect when pasting creates one large file and one ~150 byte file(?? why does this happen?)
|
||||
|
||||
- Detect when pasting an image URL, offer to download it (clientside?)
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ You can use contented as a standalone upload server, or you can use the SDK to e
|
||||
- Detect duplicate upload content and reuse storage
|
||||
- Options to limit the upload filesize and the upload bandwidth
|
||||
- Short URLs (using [url=http://hashids.org]Hashids[/url] algorithm)
|
||||
- Image thumbnailing
|
||||
|
||||
=USAGE (SERVER)=
|
||||
|
||||
@@ -26,10 +27,12 @@ You can use contented as a standalone upload server, or you can use the SDK to e
|
||||
Directory for stored content (default "")
|
||||
-db string
|
||||
Path for metadata database (default "contented.db")
|
||||
-diskFilesWorldReadable
|
||||
Save files as 0644 instead of 0600
|
||||
-enableHomepage
|
||||
Enable homepage (disable for embedded use only) (default true)
|
||||
-listen string
|
||||
(default "127.0.0.1:80")
|
||||
IP/Port to bind server (default "127.0.0.1:80")
|
||||
-max int
|
||||
Maximum size of uploaded files in MiB (set zero for unlimited) (default 8)
|
||||
-speed int
|
||||
@@ -48,6 +51,7 @@ The server responds on the following URLs:
|
||||
|
||||
- `/get/{ID}`: Download item content
|
||||
- `/info/{ID}`: Get item content metadata (JSON)
|
||||
- `/thumb/{Type}/{ID}`: Get item thumbnail image
|
||||
- `/about`: Get server metadata (JSON)
|
||||
|
||||
=USAGE (EMBEDDING FOR WEB)=
|
||||
@@ -60,6 +64,12 @@ contented.init("#target", function(/* String[] */ items) {});
|
||||
|
||||
=CHANGELOG=
|
||||
|
||||
2017-11-18: 1.2.0
|
||||
- Feature: Thumbnail support
|
||||
- Feature: File preview page
|
||||
- Feature: Album mode (via URL `/p/{file1}-{file2}-...`)
|
||||
- Feature: New `-diskFilesWorldReadable` option to save files with `0644` mode
|
||||
|
||||
2017-10-15: 1.1.0
|
||||
- Feature: Drawing mode
|
||||
- Feature: Ctrl+V image upload
|
||||
|
||||
@@ -20,15 +20,17 @@ func main() {
|
||||
maxUploadSpeed := flag.Int("speed", 0, "Maximum upload speed in bytes/sec (set zero for unlimited)")
|
||||
trustXForwardedFor := flag.Bool("trustXForwardedFor", false, "Trust X-Forwarded-For reverse proxy headers")
|
||||
enableHomepage := flag.Bool("enableHomepage", true, "Enable homepage (disable for embedded use only)")
|
||||
diskFilesWorldReadable := flag.Bool("diskFilesWorldReadable", false, "Save files as 0644 instead of 0600")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
svr, err := contented.NewServer(&contented.ServerOptions{
|
||||
DataDirectory: *dataDir,
|
||||
DBPath: *dbPath,
|
||||
BandwidthLimit: int64(*maxUploadSpeed),
|
||||
TrustXForwardedFor: *trustXForwardedFor,
|
||||
EnableHomepage: *enableHomepage,
|
||||
DataDirectory: *dataDir,
|
||||
DBPath: *dbPath,
|
||||
BandwidthLimit: int64(*maxUploadSpeed),
|
||||
TrustXForwardedFor: *trustXForwardedFor,
|
||||
EnableHomepage: *enableHomepage,
|
||||
DiskFilesWorldReadable: *diskFilesWorldReadable,
|
||||
ServerPublicProperties: contented.ServerPublicProperties{
|
||||
AppTitle: *appTitle,
|
||||
MaxUploadBytes: int64(*maxUploadMb) * 1024 * 1024,
|
||||
|
||||
97
preview.go
Normal file
97
preview.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package contented
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (this *Server) handlePreview(w http.ResponseWriter, fileIDList string) {
|
||||
|
||||
fileIDs := strings.Split(fileIDList, `-`)
|
||||
|
||||
tmpl := `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>` + html.EscapeString(this.opts.ServerPublicProperties.AppTitle) + `</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
html, body {
|
||||
background: #333;
|
||||
color: #F0F0F0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.entry {
|
||||
display: inline-block;
|
||||
margin: 4px;
|
||||
border-radius: 4px;
|
||||
max-width: 340px;
|
||||
}
|
||||
.thumbnail {
|
||||
line-height: 0;
|
||||
width: 340px;
|
||||
text-align: center;
|
||||
}
|
||||
.properties {
|
||||
background: #000;
|
||||
padding: 4px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
`
|
||||
|
||||
for _, fileID := range fileIDs {
|
||||
m, err := this.Metadata(fileID)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
http.Error(w, "Not found", 404)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println(err.Error())
|
||||
http.Error(w, "Internal error", 500)
|
||||
return
|
||||
}
|
||||
|
||||
tmpl += `
|
||||
<div class="entry">
|
||||
<div class="thumbnail">
|
||||
<a href="` + html.EscapeString(`/get/`+fileID) + `"><img src="` + html.EscapeString(`/thumb/m/`+fileID) + `"></a>
|
||||
</div>
|
||||
<div class="properties">
|
||||
<b>Name:</b> ` + html.EscapeString(m.Filename) + `<br>
|
||||
<b>Hash:</b> <span title="` + html.EscapeString(m.FileHash) + `">hover</span><br>
|
||||
<b>File type:</b> ` + html.EscapeString(m.MimeType) + `<br>
|
||||
<b>Size:</b> ` + html.EscapeString(fmt.Sprintf("%d", m.FileSize)) + `<br>
|
||||
<b>Uploader:</b> ` + html.EscapeString(m.UploadIP) + `<br>
|
||||
<b>Uploaded at:</b> ` + html.EscapeString(m.UploadTime.Format(time.RFC3339)) + `<br>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
if this.opts.EnableHomepage {
|
||||
tmpl += `
|
||||
<div class="return">
|
||||
<button onclick="window.location.href='/'">Again...</button>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
tmpl += `
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
w.Header().Set(`Content-Type`, `text/html; charset=UTF-8`)
|
||||
w.Header().Set(`Content-Length`, fmt.Sprintf("%d", len(tmpl)))
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(tmpl))
|
||||
}
|
||||
@@ -56,22 +56,7 @@ $.get("/about", function(ret) {
|
||||
|
||||
// Load upload widget
|
||||
contented.init("#surrogate-area", function(items) {
|
||||
|
||||
var $table = $("<table>");
|
||||
for (var i = 0; i < items.length; ++i) {
|
||||
$table.append($("<tr>").append([
|
||||
$("<td>").text(items[i]),
|
||||
$("<td>").html("<a target='_blank' href='" + contented.getDownloadURL(items[i]) + "'>get</a>"),
|
||||
$("<td>").html("<a target='_blank' href='" + contented.getInfoJSONURL(items[i]) + "'>info</a>"),
|
||||
]))
|
||||
}
|
||||
$("#surrogate-area").html([
|
||||
$table,
|
||||
$("<button>").addClass("again").text("Again...").click(function() {
|
||||
window.location.href = window.location.href;
|
||||
}),
|
||||
]);
|
||||
|
||||
window.location.href = contented.getMultiPreviewURL(items);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
BIN
static/nothumb_1024.png
Normal file
BIN
static/nothumb_1024.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.4 KiB |
BIN
static/nothumb_160.png
Normal file
BIN
static/nothumb_160.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
BIN
static/nothumb_340.png
Normal file
BIN
static/nothumb_340.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
BIN
static/nothumb_640.png
Normal file
BIN
static/nothumb_640.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
BIN
static/nothumb_90.png
Normal file
BIN
static/nothumb_90.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
@@ -65,13 +65,27 @@ var contented = (function() {
|
||||
},
|
||||
|
||||
"getPreviewURL": function(id) {
|
||||
return baseURL + "get/" + id; // n.b. there's no better preview URL yet
|
||||
return baseURL + "p/" + encodeURIComponent(id);
|
||||
},
|
||||
"getMultiPreviewURL": function(items) {
|
||||
return baseURL + "p/" + encodeURIComponent(items.join("-"));
|
||||
},
|
||||
"getDownloadURL": function(id) {
|
||||
return baseURL + "get/" + id;
|
||||
return baseURL + "get/" + encodeURIComponent(id);
|
||||
},
|
||||
"getInfoJSONURL": function(id) {
|
||||
return baseURL + "info/" + id;
|
||||
return baseURL + "info/" + encodeURIComponent(id);
|
||||
},
|
||||
"getThumbnailURL": function(thumbnailType, id) {
|
||||
return baseURL + "thumb/" + encodeURIComponent(thumbnailType) + "/" + encodeURIComponent(id);
|
||||
},
|
||||
"thumbnail": {
|
||||
"small_square": "s",
|
||||
"medium_square": "b",
|
||||
"medium": "t",
|
||||
"large": "m",
|
||||
"xlarge": "l",
|
||||
"xxlarge": "h"
|
||||
}
|
||||
};
|
||||
|
||||
@@ -88,21 +102,19 @@ var contented = (function() {
|
||||
};
|
||||
|
||||
var loadScripts = function(urls, onLoad) {
|
||||
if (urls.length === 0) {
|
||||
onLoad();
|
||||
return;
|
||||
}
|
||||
|
||||
var totalLoaded = 0;
|
||||
var cb = function() {
|
||||
totalLoaded += 1;
|
||||
if (totalLoaded == urls.length) {
|
||||
// load sequentially
|
||||
var i = 0;
|
||||
var loadNext = function() {
|
||||
if (i === urls.length) {
|
||||
onLoad();
|
||||
return;
|
||||
}
|
||||
|
||||
var url = urls[i];
|
||||
i += 1;
|
||||
loadScript(url, loadNext);
|
||||
};
|
||||
for (var i = 0; i < urls.length; ++i) {
|
||||
loadScript(urls[i], cb);
|
||||
}
|
||||
loadNext();
|
||||
};
|
||||
|
||||
var formatBytes = function(bytes) {
|
||||
@@ -294,6 +306,10 @@ var contented = (function() {
|
||||
|
||||
var setupDrawingBoard = function() {
|
||||
|
||||
$("head").append(
|
||||
'<link rel="stylesheet" type="text/css" href="' + contented.baseURL + 'drawingboard-0.4.6.min.css">'
|
||||
);
|
||||
|
||||
var db_id = "contented-drawing-area-" + guid();
|
||||
var $db = $("<div>").attr('id', db_id);
|
||||
|
||||
@@ -437,10 +453,10 @@ var contented = (function() {
|
||||
// Load scripts
|
||||
var needScripts = [];
|
||||
if (typeof jQuery === "undefined") {
|
||||
needScripts.push("jquery-1.12.4.min.js");
|
||||
needScripts.push(contented.baseURL + "jquery-1.12.4.min.js");
|
||||
}
|
||||
if (typeof DrawingBoard === "undefined") {
|
||||
needScripts.push("drawingboard-0.4.6.min.js");
|
||||
needScripts.push(contented.baseURL + "drawingboard-0.4.6.min.js");
|
||||
}
|
||||
|
||||
loadScripts(needScripts, afterScriptsLoaded);
|
||||
|
||||
@@ -88,12 +88,14 @@
|
||||
border-radius:8px;
|
||||
background:lightgrey;
|
||||
position:relative;
|
||||
overflow:hidden;
|
||||
}
|
||||
.contented-progress-element {
|
||||
position:absolute;
|
||||
background:darkgreen;
|
||||
left:0;
|
||||
width:0%;
|
||||
height:100%;
|
||||
}
|
||||
</style>
|
||||
<div class="contented">
|
||||
@@ -148,7 +150,6 @@
|
||||
</div>
|
||||
|
||||
<div class="contented-upload-if contented-if-drawing">
|
||||
<link rel="stylesheet" type="text/css" href="/drawingboard-0.4.6.min.css">
|
||||
<div class="contented-drawing-area"></div>
|
||||
</div>
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
78
thumb.go
Normal file
78
thumb.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package contented
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"code.ivysaur.me/thumbnail"
|
||||
)
|
||||
|
||||
func thumbnailer(t byte) (*thumbnail.Thumbnailer, error) {
|
||||
// Modelled on what imgur.com offers
|
||||
// @ref https://api.imgur.com/models/image#thumbs
|
||||
|
||||
const (
|
||||
cacheSize = 1
|
||||
outputFmt = thumbnail.OUTPUT_JPG
|
||||
scaleFmt = thumbnail.SCALEFMT_BILINEAR
|
||||
)
|
||||
|
||||
switch t {
|
||||
case 's':
|
||||
return thumbnail.NewThumbnailerEx(90, 90, cacheSize, outputFmt, thumbnail.ASPECT_CROP_TO_DIMENSIONS, scaleFmt), nil
|
||||
case 'b':
|
||||
return thumbnail.NewThumbnailerEx(160, 160, cacheSize, outputFmt, thumbnail.ASPECT_CROP_TO_DIMENSIONS, scaleFmt), nil
|
||||
case 't':
|
||||
return thumbnail.NewThumbnailerEx(160, 160, cacheSize, outputFmt, thumbnail.ASPECT_RESPECT_MAX_DIMENSION_ONLY, scaleFmt), nil
|
||||
case 'm':
|
||||
return thumbnail.NewThumbnailerEx(340, 340, cacheSize, outputFmt, thumbnail.ASPECT_RESPECT_MAX_DIMENSION_ONLY, scaleFmt), nil
|
||||
case 'l':
|
||||
return thumbnail.NewThumbnailerEx(640, 640, cacheSize, outputFmt, thumbnail.ASPECT_RESPECT_MAX_DIMENSION_ONLY, scaleFmt), nil
|
||||
case 'h':
|
||||
return thumbnail.NewThumbnailerEx(1024, 1024, cacheSize, outputFmt, thumbnail.ASPECT_RESPECT_MAX_DIMENSION_ONLY, scaleFmt), nil
|
||||
default:
|
||||
return nil, errors.New("Unsupported thumbnail type (should be s/b/t/m/l/h)")
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Server) handleThumb(w http.ResponseWriter, r *http.Request, thumbnailType byte, fileId string) {
|
||||
t, err := thumbnailer(thumbnailType)
|
||||
if err != nil {
|
||||
log.Printf("%s Thumbnail failed: %s\n", this.remoteIP(r), err.Error())
|
||||
http.Error(w, err.Error(), 400)
|
||||
return
|
||||
}
|
||||
|
||||
err = this.handleThumbInternal(w, t, fileId)
|
||||
if err != nil {
|
||||
log.Printf("%s Thumbnail failed: %s\n", this.remoteIP(r), err.Error())
|
||||
|
||||
w.Header().Set(`Location`, fmt.Sprintf(`/nothumb_%d.png`, t.Height()))
|
||||
w.WriteHeader(302)
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Server) handleThumbInternal(w http.ResponseWriter, t *thumbnail.Thumbnailer, fileId string) error {
|
||||
|
||||
// Load metadata
|
||||
m, err := this.Metadata(fileId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filePath := filepath.Join(this.opts.DataDirectory, m.FileHash)
|
||||
thumb, err := t.RenderFile_NoCache_MimeType(filePath, m.MimeType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.Header().Set(`Content-Length`, fmt.Sprintf("%d", len(thumb)))
|
||||
w.Header().Set(`Content-Type`, `image/jpeg`)
|
||||
w.WriteHeader(200)
|
||||
w.Write(thumb)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -91,7 +91,7 @@ func (this *Server) handleUploadFile(src multipart.File, hdr *multipart.FileHead
|
||||
|
||||
// Save file to disk
|
||||
fileHash := hex.EncodeToString(hasher.Sum(nil))
|
||||
dest, err := os.OpenFile(filepath.Join(this.opts.DataDirectory, fileHash), os.O_CREATE|os.O_WRONLY, 0600)
|
||||
dest, err := os.OpenFile(filepath.Join(this.opts.DataDirectory, fileHash), os.O_CREATE|os.O_WRONLY, this.opts.FileMode())
|
||||
shouldSave := true
|
||||
if err != nil && os.IsExist(err) {
|
||||
// hash matches existing upload
|
||||
|
||||
Reference in New Issue
Block a user