6 Commits

Author SHA1 Message Date
codeskyblue
9a950b98db modify part 2018-11-28 09:58:54 +08:00
gzliuxin
033f0a9271 fix dldcounter bug 2018-11-26 18:24:44 +08:00
gzliuxin
1f68fcb45f add dldcounter to valid ip address 2018-11-26 15:05:59 +08:00
gzliuxin
b81894b0cf add autosave interval; add column for downlowads in index.html 2018-11-26 13:32:26 +08:00
gzliuxin
cfaa27b981 add db.go 2018-11-26 11:59:28 +08:00
gzliuxin
ffcf9cc742 add download count 2018-11-26 11:54:09 +08:00
28 changed files with 511 additions and 1012 deletions

View File

@@ -1,34 +0,0 @@
name: goreleaser
on:
push:
# run only against tags
tags:
- '*'
permissions:
contents: write
# packages: write
# issues: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Fetch all tags
run: git fetch --force --tags
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.18
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
with:
# either 'goreleaser' (default) or 'goreleaser-pro'
distribution: goreleaser
version: latest
args: release
env:
GITHUB_TOKEN: ${{ secrets.TOKEN }}

4
.gitignore vendored
View File

@@ -29,6 +29,4 @@ bindata_assetfs.go
assets_vfsdata.go
*.un~
*.swp
dist/
.DS_Store
ghs-stats.json

View File

@@ -1,32 +1,44 @@
version: 2
before:
hooks:
- go mod tidy
project_name: gohttpserver
release:
github:
owner: codeskyblue
name: gohttpserver
brew:
github:
owner: codeskyblue
name: homebrew-tap
homepage: https://github.com/codeskyblue/gohttpserver
builds:
- env:
- CGO_ENABLED=0
main: .
ldflags: -s -w -X main.VERSION={{.Version}} -X main.BUILDTIME={{.Date}} -X main.GITCOMMIT={{.Commit}}
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm64
- "386"
goarm:
- "6"
archives:
- format: tar.gz
# use zip for windows archives
format_overrides:
- goos: windows
format: zip
# brews:
# - tap:
# owner: codeskyblue
# name: homebrew-tap
# folder: Formula
- goos:
- linux
- darwin
- windows
goarch:
- amd64
- "386"
goarm:
- "6"
main: .
ldflags: -s -w -X main.VERSION={{.Version}}
flags: -tags vfs
binary: gohttpserver
hooks:
pre: go generate .
archive:
format: zip
name_template: '{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{
.Arm }}{{ end }}'
files:
- licence*
- LICENCE*
- license*
- LICENSE*
- readme*
- README*
- changelog*
- CHANGELOG*
- .ghs.yml
snapshot:
name_template: SNAPSHOT-{{ .Commit }}
checksum:
name_template: '{{ .ProjectName }}_{{ .Version }}_checksums.txt'

26
.travis.yml Normal file
View File

@@ -0,0 +1,26 @@
sudo: required
services:
- docker
language: go
go:
- "1.11"
env:
- GO111MODULE=on
script:
- go test -v
addons:
apt:
packages:
- docker-ce
deploy:
- provider: script
skip_cleanup: true
script: curl -sL https://git.io/goreleaser | bash
on:
tags: true
condition: $TRAVIS_OS_NAME = linux
- provider: script
skip_cleanup: true
script: bash docker/push_images && bash docker/push_manifest
on:
branch: master

106
README.md
View File

@@ -1,6 +1,5 @@
# gohttpserver
[![Build Status](https://travis-ci.org/codeskyblue/gohttpserver.svg?branch=master)](https://travis-ci.org/codeskyblue/gohttpserver)
[![Docker Automated build](https://img.shields.io/docker/automated/codeskyblue/gohttpserver)](https://hub.docker.com/repository/docker/codeskyblue/gohttpserver)
- Goal: Make the best HTTP File Server.
- Features: Human-friendly UI, file uploading support, direct QR-code generation for Apple & Android install package.
@@ -13,7 +12,7 @@
**Binaries** can be downloaded from [this repo releases](https://github.com/codeskyblue/gohttpserver/releases/)
## Requirements
Tested with go-1.16
Tested with go-1.10, go-1.11
## Screenshots
![screen](testdata/filetypes/gohttpserver.gif)
@@ -55,26 +54,21 @@ Tested with go-1.16
1. [x] Show folder size
1. [x] Create folder
1. [x] Skip delete confirm when alt pressed
1. [x] Support unzip zip file when upload(with form: unzip=true)
## Installation
```bash
$ go install github.com/codeskyblue/gohttpserver@latest
```
go get -v github.com/codeskyblue/gohttpserver
cd $GOPATH/src/github.com/codeskyblue/gohttpserver
go build && ./gohttpserver
```
Or download binaries from [github releases](https://github.com/codeskyblue/gohttpserver/releases)
If you are using Mac, simply run command
```bash
$ brew install codeskyblue/tap/gohttpserver
```
## Usage
Listen on port 8000 of all interfaces, and enable file uploading.
```
$ gohttpserver -r ./ --port 8000 --upload
./gohttpserver -r ./ --port 8000 --upload
```
Use command `gohttpserver --help` to see more usage.
@@ -83,37 +77,30 @@ Use command `gohttpserver --help` to see more usage.
share current directory
```bash
$ docker run -it --rm -p 8000:8000 -v $PWD:/app/public --name gohttpserver codeskyblue/gohttpserver
docker run -it --rm -p 8000:8000 -v $PWD:/app/public --name gohttpserver codeskyblue/gohttpserver
```
Share current directory with http basic auth
```bash
$ docker run -it --rm -p 8000:8000 -v $PWD:/app/public --name gohttpserver \
docker run -it --rm -p 8000:8000 -v $PWD:/app/public --name gohttpserver \
codeskyblue/gohttpserver \
--auth-type http --auth-http username1:password1 --auth-http username2:password2
--auth-type http --auth-http username:password
```
Share current directory with openid auth. (Works only in netease company.)
```bash
$ docker run -it --rm -p 8000:8000 -v $PWD:/app/public --name gohttpserver \
docker run -it --rm -p 8000:8000 -v $PWD:/app/public --name gohttpserver \
codeskyblue/gohttpserver \
--auth-type openid
```
To build image yourself, please change the PWD to the root of this repo.
```bash
$ cd gohttpserver/
$ docker build -t codeskyblue/gohttpserver -f docker/Dockerfile .
```
## Authentication options
- Enable basic http authentication
```sh
$ gohttpserver --auth-type http --auth-http username1:password1 --auth-http username2:password2
$ gohttpserver --auth-type http --auth-http username:password
```
- Use openid auth
@@ -122,24 +109,6 @@ $ docker build -t codeskyblue/gohttpserver -f docker/Dockerfile .
$ gohttpserver --auth-type openid --auth-openid https://login.example-hostname.com/openid/
```
- Use oauth2-proxy with
```sh
$ gohttpserver --auth-type oauth2-proxy
```
You can configure to let a http reverse proxy handling authentication.
When using oauth2-proxy, the backend will use identification info from request headers `X-Auth-Request-Email` as userId and `X-Auth-Request-Fullname` as user's display name.
Please config your oauth2 reverse proxy yourself.
More about [oauth2-proxy](https://github.com/oauth2-proxy/oauth2-proxy).
All required headers list as following.
|header|value|
|---|---|
|X-Auth-Request-Email| userId |
|X-Auth-Request-Fullname| user's display name(urlencoded) |
|X-Auth-Request-User| user's nickname (mostly email prefix) |
- Enable upload
```sh
@@ -196,8 +165,8 @@ accessTables:
### ipa plist proxy
This is used for server on which https is enabled. default use <https://plistproxy.herokuapp.com/plist>
```bash
$ gohttpserver --plistproxy=https://someproxyhost.com/
```
./gohttpserver --plistproxy=https://someproxyhost.com/
```
Test if proxy works:
@@ -211,8 +180,6 @@ $ http GET https://someproxyhost.com/plist/18f99211
# show the app.plist content
```
If your ghs running behide nginx server and have https configed. plistproxy will be disabled automaticly.
### Upload with CURL
For example, upload a file named `foo.txt` to directory `somedir`
@@ -228,13 +195,6 @@ $ curl -F file=@foo.txt -F filename=hi.txt localhost:8000/somedir
{"destination":"somedir/hi.txt","success":true}
```
Upload zip file and unzip it (zip file will be delete when finished unzip)
```
$ curl -F file=@pkg.zip -F unzip=true localhost:8000/somedir
{"success": true}
```
Note: `\/:*<>|` are not allowed in filenames.
### Deploy with nginx
@@ -258,39 +218,8 @@ server {
}
```
gohttpserver should started with `--xheaders` argument when behide nginx.
Refs: <http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size>
gohttpserver also support `--prefix` flag which will help to when meet `/` is occupied by other service. relative issue <https://github.com/codeskyblue/gohttpserver/issues/105>
Usage example:
```bash
# for gohttpserver
$ gohttpserver --prefix /foo --addr :8200 --xheaders
```
**Nginx settigns**
```
server {
listen 80;
server_name your-domain-name.com;
location /foo {
proxy_pass http://127.0.0.1:8200; # here need to change
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 0; # disable upload limit
}
}
```
## FAQ
- [How to generate self signed certificate with openssl](http://stackoverflow.com/questions/10175812/how-to-create-a-self-signed-certificate-with-openssl)
@@ -306,13 +235,14 @@ Depdencies are managed by [govendor](https://github.com/kardianos/govendor)
1. Build develop version. **assets** directory must exists
```sh
$ go build
$ ./gohttpserver
go build
./gohttpserver
```
2. Build single binary release
```sh
$ go build
go generate .
go build -tags vfs
```
Theme are defined in [assets/themes](assets/themes) directory. Now only two themes are available, "black" and "green".
@@ -332,7 +262,7 @@ Theme are defined in [assets/themes](assets/themes) directory. Now only two them
**Go Libraries**
* [vfsgen](https://github.com/shurcooL/vfsgen) Not using now
* [vfsgen](https://github.com/shurcooL/vfsgen)
* [go-bindata-assetfs](https://github.com/elazarl/go-bindata-assetfs) Not using now
* <http://www.gorillatoolkit.org/pkg/handlers>

View File

@@ -1,12 +0,0 @@
package main
import (
"embed"
"net/http"
)
//go:embed assets
var assetsFS embed.FS
// Assets contains project assets.
var Assets = http.FS(assetsFS)

View File

@@ -4,16 +4,15 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta name="theme-color" content="#000000">
<title>[[.Title]]</title>
<link rel="shortcut icon" type="image/png" href="[[.Prefix]]/-/assets/favicon.png" />
<link rel="stylesheet" type="text/css" href="[[.Prefix]]/-/assets/bootstrap-3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="[[.Prefix]]/-/assets/font-awesome-4.6.3/css/font-awesome.min.css">
<link rel="stylesheet" type="text/css" href="[[.Prefix]]/-/assets/css/github-markdown.css">
<link rel="stylesheet" type="text/css" href="[[.Prefix]]/-/assets/css/dropzone.css">
<link rel="stylesheet" type="text/css" href="[[.Prefix]]/-/assets/css/scrollUp-image.css">
<link rel="stylesheet" type="text/css" href="[[.Prefix]]/-/assets/css/style.css">
<link rel="stylesheet" type="text/css" href="[[.Prefix]]/-/assets/themes/[[.Theme]].css">
<title>gohttp server</title>
<link rel="shortcut icon" type="image/png" href="/-/assets/favicon.png" />
<link rel="stylesheet" type="text/css" href="/-/assets/bootstrap-3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/-/assets/font-awesome-4.6.3/css/font-awesome.min.css">
<link rel="stylesheet" type="text/css" href="/-/assets/css/github-markdown.css">
<link rel="stylesheet" type="text/css" href="/-/assets/css/dropzone.css">
<link rel="stylesheet" type="text/css" href="/-/assets/css/scrollUp-image.css">
<link rel="stylesheet" type="text/css" href="/-/assets/css/style.css">
<link rel="stylesheet" type="text/css" href="/-/assets/themes/[[.Theme]].css">
</head>
<body id="app">
@@ -27,7 +26,7 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="[[.Prefix]]/">[[.Title]]</a>
<a class="navbar-brand" href="/">[[.Title]]</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-2">
<ul class="nav navbar-nav">
@@ -50,34 +49,18 @@
</a>
</template>
[[end]]
[[if eq .AuthType "oauth2-proxy"]]
<template v-if="!user.email">
<a href="#" class="btn btn-sm btn-default navbar-btn">
Guest <span class="glyphicon glyphicon-user"></span>
</a>
</template>
<template v-else>
<a href="/-/logout" class="btn btn-sm btn-default navbar-btn">
<span v-text="user.name"></span>
<i class="fa fa-sign-out"></i>
</a>
</template>
[[end]]
</ul>
[[if not .NoIndex ]]
<form class="navbar-form navbar-right">
<div class="input-group">
<input type="text" name="search" class="form-control" placeholder="Search text" v-bind:value="search"
autofocus>
<span class="input-group-btn">
<button class="btn btn-default" type="submit">
<button class="btn btn-default" type="button">
<span class="glyphicon glyphicon-search"></span>
</button>
</span>
</div>
</form>
[[end]]
<ul id="nav-right-bar" class="nav navbar-nav navbar-right">
</ul>
</div>
@@ -126,13 +109,14 @@
<th class="hidden-xs">
<span style="cursor: pointer" v-on:click='mtimeTypeFromNow = !mtimeTypeFromNow'>ModTime</span>
</th>
<th>Downloads</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="f in computedFiles">
<td>
<a v-on:click='clickFileOrDir(f, $event)' href="{{getEncodePath(f.name)}}">
<a v-on:click='clickFileOrDir(f, $event)' href="/{{f.path + (f.type == 'dir' ? '' : '')}}">
<!-- ?raw=false -->
<i style="padding-right: 0.5em" class="fa" v-bind:class='genFileClass(f)'></i> {{f.name}}
</a>
@@ -143,21 +127,19 @@
</td>
<td><span v-if="f.type == 'dir'">~</span> {{f.size | formatBytes}}</td>
<td class="hidden-xs">{{formatTime(f.mtime)}}</td>
<td class="hidden-xs">{{f.dldcnt}}</td>
<td style="text-align: left">
<template v-if="f.type == 'dir'">
<a class="btn btn-default btn-xs" href="{{getEncodePath(f.name)}}/?op=archive">
<a class="btn btn-default btn-xs" href="/-/zip/{{f.path}}">
<span class="hidden-xs">Archive</span> Zip
<span class="glyphicon glyphicon-download-alt"></span>
</a>
<button class="btn btn-default btn-xs" v-on:click="showInfo(f)">
<span class="glyphicon glyphicon-info-sign"></span>
</button>
<button class="btn btn-default btn-xs" v-if="auth.delete" v-on:click="deletePathConfirm(f, $event)">
<span style="color:#CC3300" class="glyphicon glyphicon-trash"></span>
</button>
</template>
<template v-if="f.type == 'file'">
<a class="btn btn-default btn-xs hidden-xs" href="{{genDownloadURL(f)}}">
<a class="btn btn-default btn-xs hidden-xs" href="/{{f.path}}?download=true">
<span class="hidden-xs">Download</span>
<span class="glyphicon glyphicon-download-alt"></span>
</a>
@@ -259,31 +241,24 @@
</div>
<div class="col-md-12">
<div id="footer" class="pull-right" style="margin: 2em 1em">
<a href="https://github.com/codeskyblue/gohttpserver">gohttpserver ({{version}})</a>, by <a href="https://github.com/codeskyblue">codeskyblue</a>.
<a href="https://github.com/codeskyblue/gohttpserver">gohttpserver (ver:{{version}})</a>, written by <a href="https://github.com/codeskyblue">codeskyblue</a>.
Copyright 2016-2018. go1.10
</div>
</div>
</div>
<script>
window.URL_PFEFIX = "[[.Prefix]]"
</script>
<script src="[[.Prefix]]/-/assets/js/jquery-3.1.0.min.js"></script>
<script src="[[.Prefix]]/-/assets/js/jquery.qrcode.js"></script>
<script src="[[.Prefix]]/-/assets/js/jquery.scrollUp.min.js"></script>
<script src="[[.Prefix]]/-/assets/js/qrcode.js"></script>
<script src="[[.Prefix]]/-/assets/js/vue-1.0.min.js"></script>
<script src="[[.Prefix]]/-/assets/js/showdown-1.6.4.min.js"></script>
<script src="[[.Prefix]]/-/assets/js/moment.min.js"></script>
<script src="[[.Prefix]]/-/assets/js/dropzone.js"></script>
<script src="[[.Prefix]]/-/assets/js/underscore-min.js"></script>
<script src="[[.Prefix]]/-/assets/js/clipboard-1.5.12.min.js"></script>
<script src="[[.Prefix]]/-/assets/bootstrap-3.3.5/js/bootstrap.min.js"></script>
<script src='[[.Prefix]]/-/assets/[["js/index.js" | urlhash ]]'></script>
<!-- <script src="[[.Prefix]]/-/assets/js/index.js"></script> -->
<!--Sync status bar color with border-color on mobile platforms.-->
<script>
var META = document.getElementsByTagName("meta");
META[2]["content"]=$('.navbar').css('border-color');
</script>
<script src="/-/assets/js/jquery-3.1.0.min.js"></script>
<script src="/-/assets/js/jquery.qrcode.js"></script>
<script src="/-/assets/js/jquery.scrollUp.min.js"></script>
<script src="/-/assets/js/qrcode.js"></script>
<script src="/-/assets/js/vue-1.0.min.js"></script>
<script src="/-/assets/js/showdown-1.6.4.min.js"></script>
<script src="/-/assets/js/moment.min.js"></script>
<script src="/-/assets/js/dropzone.js"></script>
<script src="/-/assets/js/underscore-min.js"></script>
<script src="/-/assets/js/clipboard-1.5.12.min.js"></script>
<script src="/-/assets/bootstrap-3.3.5/js/bootstrap.min.js"></script>
<script src='/-/assets/[["js/index.js" | urlhash ]]'></script>
<!-- <script src="/-/assets/js/index.js"></script> -->
[[if .GoogleTrackerID ]]
<script>
(function (i, s, o, g, r, a, m) {
@@ -303,4 +278,4 @@
</script> [[ end ]]
</body>
</html>
</html>

View File

@@ -21,21 +21,6 @@ function getQueryString(name) {
return null;
}
function checkPathNameLegal(name) {
var reg = new RegExp("[\\/:*<>|]");
var r = name.match(reg)
return r == null;
}
function showErrorMessage(jqXHR) {
let errMsg = jqXHR.getResponseHeader("x-auth-authentication-message")
if (errMsg == null) {
errMsg = jqXHR.responseText
}
alert(String(jqXHR.status).concat(":", errMsg));
console.error(errMsg)
}
var vm = new Vue({
el: "#app",
data: {
@@ -140,9 +125,6 @@ var vm = new Vue({
});
},
methods: {
getEncodePath: function (filepath) {
return pathJoin([location.pathname].concat(filepath.split("/").map(v => encodeURIComponent(v))))
},
formatTime: function (timestamp) {
var m = moment(timestamp);
if (this.mtimeTypeFromNow) {
@@ -169,7 +151,7 @@ var vm = new Vue({
if (!name) {
parts.push(pathname);
} else if (getExtention(name) == "ipa") {
parts.push("/-/ipa/link", pathname, encodeURIComponent(name));
parts.push("/-/ipa/link", pathname, name);
} else {
parts.push(pathname, name);
}
@@ -188,9 +170,7 @@ var vm = new Vue({
$("#qrcode-modal").modal("show");
},
genDownloadURL: function (f) {
var search = location.search;
var sep = search == "" ? "?" : "&"
return location.origin + this.getEncodePath(f.name) + location.search + sep + "download=true";
return location.origin + "/" + f.path;
},
shouldHaveQrcode: function (name) {
return ['apk', 'ipa'].indexOf(getExtention(name)) !== -1;
@@ -236,20 +216,11 @@ var vm = new Vue({
return "fa-file-text-o"
},
clickFileOrDir: function (f, e) {
var reqPath = this.getEncodePath(f.name)
// TODO: fix here tomorrow
if (f.type == "file") {
// check whether the file is video
var videoExtensions = ['mp4', 'webm', 'ogg', 'mov', 'avi', 'mkv'];
var fileExtension = getExtention(f.name).toLowerCase();
if (videoExtensions.includes(fileExtension)) {
window.location.href = '/-/video-player' + reqPath;
} else {
window.location.href = reqPath;
}
e.preventDefault()
return;
return true;
}
var reqPath = pathJoin([location.pathname, f.name]);
loadFileOrDir(reqPath);
e.preventDefault()
},
@@ -260,41 +231,34 @@ var vm = new Vue({
showInfo: function (f) {
console.log(f);
$.ajax({
url: this.getEncodePath(f.name),
data: {
op: "info",
},
url: pathJoin(["/-/info", location.pathname, f.name]),
method: "GET",
success: function (res) {
$("#file-info-title").text(f.name);
$("#file-info-content").text(JSON.stringify(res, null, 4));
$("#file-info-modal").modal("show");
// console.log(JSON.stringify(res, null, 4));
},
error: function (jqXHR, textStatus, errorThrown) {
showErrorMessage(jqXHR)
}
})
},
makeDirectory: function () {
var name = window.prompt("current path: " + location.pathname + "\nplease enter the new directory name", "")
var name = window.prompt("Directory name?")
console.log(name)
if (!name) {
return
}
if(!checkPathNameLegal(name)) {
alert("Name should not contains any of \\/:*<>|")
return
}
$.ajax({
url: this.getEncodePath(name),
url: pathJoin(["/-/mkdir", location.pathname]),
data: {
name: name,
},
method: "POST",
success: function (res) {
console.log(res)
loadFileList()
},
error: function (jqXHR, textStatus, errorThrown) {
showErrorMessage(jqXHR)
error: function (err) {
alert(err.responseText);
}
})
},
@@ -306,19 +270,18 @@ var vm = new Vue({
}
}
$.ajax({
url: this.getEncodePath(f.name),
url: pathJoin([location.pathname, f.name]),
method: 'DELETE',
success: function (res) {
loadFileList()
},
error: function (jqXHR, textStatus, errorThrown) {
showErrorMessage(jqXHR)
error: function (err) {
alert(err.responseText);
}
});
},
updateBreadcrumb: function (pathname) {
var pathname = decodeURI(pathname || location.pathname || "/");
pathname = pathname.split('?')[0]
updateBreadcrumb: function () {
var pathname = decodeURI(location.pathname || "/");
var parts = pathname.split('/');
this.breadcrumb = [];
if (pathname == "/") {
@@ -344,23 +307,23 @@ var vm = new Vue({
}
var that = this;
$.getJSON(pathJoin(['/-/info', location.pathname]))
.then(function (res) {
console.log(res);
that.preview.filename = res.name;
that.preview.filesize = res.size;
return $.ajax({
url: '/' + res.path,
dataType: 'text',
});
})
.then(function (res) {
console.log(res)
that.preview.contentHTML = '<pre>' + res + '</pre>';
console.log("Finally")
})
.done(function (res) {
console.log("done", res)
.then(function (res) {
console.log(res);
that.preview.filename = res.name;
that.preview.filesize = res.size;
return $.ajax({
url: '/' + res.path,
dataType: 'text',
});
})
.then(function (res) {
console.log(res)
that.preview.contentHTML = '<pre>' + res + '</pre>';
console.log("Finally")
})
.done(function (res) {
console.log("done", res)
});
},
loadAll: function () {
// TODO: move loadFileList here
@@ -377,23 +340,16 @@ window.onpopstate = function (event) {
}
function loadFileOrDir(reqPath) {
let requestUri = reqPath + location.search
var retObj = loadFileList(requestUri)
if (retObj !== null) {
retObj.done(function () {
window.history.pushState({}, "", requestUri);
});
}
window.history.pushState({}, "", reqPath);
loadFileList(reqPath)
}
function loadFileList(pathname) {
var pathname = pathname || location.pathname + location.search;
var retObj = null
var pathname = pathname || location.pathname;
// console.log("load filelist:", pathname)
if (getQueryString("raw") !== "false") { // not a file preview
var sep = pathname.indexOf("?") === -1 ? "?" : "&"
retObj = $.ajax({
url: pathname + sep + "json=true",
$.ajax({
url: pathJoin(["/-/json", pathname]),
dataType: "json",
cache: false,
success: function (res) {
@@ -401,22 +357,22 @@ function loadFileList(pathname) {
var weight = f.type == 'dir' ? 1000 : 1;
return -weight * f.mtime;
})
vm.files = res.files;
vm.auth = res.auth;
vm.updateBreadcrumb(pathname);
},
error: function (jqXHR, textStatus, errorThrown) {
showErrorMessage(jqXHR)
error: function (err) {
console.error(err)
},
});
}
vm.updateBreadcrumb();
vm.previewMode = getQueryString("raw") == "false";
if (vm.previewMode) {
vm.loadPreviewFile();
}
return retObj
}
Vue.filter('fromNow', function (value) {
@@ -451,10 +407,10 @@ $(function () {
console.info('Text:', e.text);
console.info('Trigger:', e.trigger);
$(e.trigger)
.tooltip('show')
.mouseleave(function () {
$(this).tooltip('hide');
})
.tooltip('show')
.mouseleave(function () {
$(this).tooltip('hide');
})
e.clearSelection();
});

View File

@@ -1,31 +0,0 @@
body {}
td>a:hover {
color: #4cc0cf;
font-weight: normal;
}
td>a {
color: rgb(51, 51, 51);
}
a:hover {
font-weight: bold;
}
.navbar {
background-color: #00BCD4;
border-color: #0097A7;
}
.navbar .navbar-brand {
color: white;
}
.navbar {
border-radius: 0px;
}
.navbar-default ul.navbar-nav>li>a {
color: white;
}

View File

@@ -1,49 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Video Player - [[.FileName]]</title>
<style>
body, html {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
overflow: hidden;
background-color: #000;
}
.video-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
}
video {
max-width: 100%;
max-height: 100%;
}
h1 {
color: #fff;
font-size: 24px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="video-container">
<video id="videoPlayer" controls autoplay>
<source src="[[.VideoURL]]" type="video/[[.Extension]]">
Your browser does not support the video tag.
</video>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var video = document.getElementById('videoPlayer');
video.focus();
});
</script>
</body>
</html>

9
assets_dev.go Normal file
View File

@@ -0,0 +1,9 @@
// +build !vfs
//go:generate go run assets_generate.go
package main
import "net/http"
// Assets contains project assets.
var Assets http.FileSystem = http.Dir("assets")

23
assets_generate.go Normal file
View File

@@ -0,0 +1,23 @@
// +build ignore
package main
import (
"log"
"net/http"
"github.com/shurcooL/vfsgen"
)
func main() {
var fs http.FileSystem = http.Dir("assets")
err := vfsgen.Generate(fs, vfsgen.Options{
PackageName: "main",
BuildTags: "vfs",
VariableName: "Assets",
})
if err != nil {
log.Fatalln(err)
}
}

View File

@@ -23,10 +23,13 @@ fi
build() {
echo "$1 $2 ..."
GOOS=$1 GOARCH=$2 go build \
-tags bindata \
-ldflags "$LDFLAGS" \
-o dist/gohttpserver-${3:-""}
}
go-bindata-assetfs -tags bindata res/...
build linux arm linux-arm
build darwin amd64 mac-amd64
build linux amd64 linux-amd64

107
db.go Normal file
View File

@@ -0,0 +1,107 @@
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"time"
)
func panicErr(e error) {
if e != nil {
panic(e)
}
}
type Model struct {
Filename string
Data *map[string]int
LastSave time.Time
}
var DBModel Model = Model{}
// var SaveInterval float64 = 60.0 // seconds
const SaveInterval = 60.0
func (model *Model) DBRead(filename string) *map[string]int {
data := make(map[string]int)
model.Filename = filename
model.Data = &data
// create if file not exist
if _, err := os.Stat(filename); os.IsNotExist(err) {
f, cerr := os.Create(filename)
panicErr(cerr)
s, jerr := json.Marshal(data)
panicErr(jerr)
f.Write(s)
defer f.Close()
return &data
}
dat, err := ioutil.ReadFile(filename)
panicErr(err)
err = json.Unmarshal(dat, &data)
panicErr(err)
fmt.Println(data)
return &data
}
func (model *Model) DBWrite() error {
fmt.Println("db writing")
dat, err := json.Marshal(model.Data)
if err != nil {
return err
}
tmp := model.Filename + ".un~"
err = ioutil.WriteFile(tmp, dat, 0644)
if err != nil {
return err
}
return os.Rename(tmp, model.Filename)
}
func (model *Model) Incre(key string) {
(*model.Data)[key]++
fmt.Println(model.Data)
go model.AutoSave()
}
func (model *Model) AutoSave() bool {
if time.Since(model.LastSave).Seconds() > SaveInterval {
model.DBWrite()
model.LastSave = time.Now()
return true
}
return false
}
type dldEntry struct {
ip string
path string
}
// DldCounter is used to validate duplicated download requests
type DldCounter struct {
IPHistory map[dldEntry]time.Time
}
var DCounter DldCounter = DldCounter{}
var DldInterval float64 = 60.0 // seconds
func (counter *DldCounter) Validate(ip string, path string) bool {
if counter.IPHistory == nil {
counter.IPHistory = make(map[dldEntry]time.Time)
}
entry := dldEntry{ip, path}
if time.Since(counter.IPHistory[entry]).Seconds() > DldInterval {
counter.IPHistory[entry] = time.Now()
return true
}
return false
}

View File

@@ -1,15 +1,16 @@
FROM golang:1.16
WORKDIR /app/gohttpserver
ADD . /app/gohttpserver
FROM golang:1.10
WORKDIR /go/src/github.com/codeskyblue/gohttpserver
ADD . /go/src/github.com/codeskyblue/gohttpserver/
RUN go get -v
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags '-X main.VERSION=docker' -o gohttpserver
FROM debian:stable
FROM debian:stretch
WORKDIR /app
RUN mkdir -p /app/public
RUN apt-get update && apt-get install -y ca-certificates
VOLUME /app/public
ADD assets ./assets
COPY --from=0 /app/gohttpserver/gohttpserver .
COPY --from=0 /go/src/github.com/codeskyblue/gohttpserver/gohttpserver .
EXPOSE 8000
ENTRYPOINT [ "/app/gohttpserver", "--root=/app/public" ]
CMD []

View File

@@ -1,19 +0,0 @@
FROM golang:1.16.0-alpine as build
WORKDIR /app
COPY go.mod .
COPY go.sum .
RUN go mod download
# Binary Compression
RUN apk add upx --no-cache
# Build app and pack using upx
ADD . /app
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags '-s -w -X main.VERSION=docker' -o gohttpserver && upx gohttpserver
FROM alpine:3
WORKDIR /app
ADD assets /usr/local/bin/assets
COPY --from=build /app/gohttpserver /usr/local/bin/gohttpserver
EXPOSE 8000
ENTRYPOINT [ "/usr/local/bin/gohttpserver" ]

View File

@@ -1,6 +1,7 @@
FROM golang:1.16
WORKDIR /appsrc/gohttpserver
ADD . /appsrc/gohttpserver
FROM golang:1.10
WORKDIR /go/src/github.com/codeskyblue/gohttpserver
ADD . /go/src/github.com/codeskyblue/gohttpserver/
RUN go get -v
RUN GOOS=linux GOARCH=arm go build -ldflags '-X main.VERSION=docker' -o gohttpserver .
FROM multiarch/debian-debootstrap:armhf-stretch
@@ -9,7 +10,7 @@ RUN mkdir -p /app/public
RUN apt-get update && apt-get install -y ca-certificates
VOLUME /app/public
ADD assets ./assets
COPY --from=0 /appsrc/gohttpserver/gohttpserver .
COPY --from=0 /go/src/github.com/codeskyblue/gohttpserver/gohttpserver .
EXPOSE 8000
ENTRYPOINT [ "/app/gohttpserver", "--root=/app/public" ]
CMD []

View File

@@ -9,12 +9,8 @@ fi
set -ex
if test $(uname) = "Linux"
then
sed -i '/experimental/d' $HOME/.docker/config.json
sed -i '1a"experimental": "enabled",' $HOME/.docker/config.json
fi
sed -i '/experimental/d' $HOME/.docker/config.json
sed -i '1a"experimental": "enabled",' $HOME/.docker/config.json
docker manifest create codeskyblue/gohttpserver \
codeskyblue/gohttpserver:latest \
codeskyblue/gohttpserver:armhf
@@ -25,4 +21,4 @@ docker manifest annotate codeskyblue/gohttpserver \
docker manifest push codeskyblue/gohttpserver
# check again
docker run mplatform/mquery codeskyblue/gohttpserver
docker run mplatform/mquery codeskyblue/gohttpserver

16
go.mod
View File

@@ -1,11 +1,9 @@
module github.com/codeskyblue/gohttpserver
go 1.16
require (
github.com/alecthomas/kingpin v2.2.6+incompatible
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf
github.com/codeskyblue/dockerignore v0.0.0-20151214070507-de82dee623d9
github.com/codeskyblue/go-accesslog v0.0.0-20171215023101-6188d3bd9371
github.com/codeskyblue/openid-go v0.0.0-20160923065855-0d30842b2fb4
@@ -15,11 +13,9 @@ require (
github.com/gorilla/handlers v1.4.0
github.com/gorilla/mux v1.6.2
github.com/gorilla/sessions v1.1.3
github.com/pkg/errors v0.8.0 // indirect
github.com/pkg/errors v0.8.0
github.com/shogo82148/androidbinary v0.0.0-20180627093851-01c4bfa8b3b5
github.com/smartystreets/goconvey v1.6.4 // indirect
github.com/stretchr/testify v1.3.0
golang.org/x/net v0.23.0 // indirect
golang.org/x/text v0.14.0
howett.net/plist v0.0.0-20201203080718-1454fab16a06 // indirect
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371
github.com/shurcooL/vfsgen v0.0.0-20181020040650-a97a25d856ca
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a
)

107
go.sum
View File

@@ -1,107 +0,0 @@
github.com/alecthomas/kingpin v2.2.6+incompatible h1:5svnBTFgJjZvGKyYBtMB0+m5wvrbUHiqye8wRJMlnYI=
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/codeskyblue/dockerignore v0.0.0-20151214070507-de82dee623d9 h1:c9axcChJwkLuSl9AvwTHi8jiBa6+VX4gGgERhABgv2E=
github.com/codeskyblue/dockerignore v0.0.0-20151214070507-de82dee623d9/go.mod h1:XNZkUhPf+qgRnhY/ecS3B73ODJ2NXCzDMJHXM069IMg=
github.com/codeskyblue/go-accesslog v0.0.0-20171215023101-6188d3bd9371 h1:dEBIvaVFaP2Uc9QA6J41qWxE5NfEnDWEBk+kWv5nK5k=
github.com/codeskyblue/go-accesslog v0.0.0-20171215023101-6188d3bd9371/go.mod h1:sgXnVxxZ1u72GAzc9s1SzpuPMxBDKfTg6F2PvDrPSJU=
github.com/codeskyblue/openid-go v0.0.0-20160923065855-0d30842b2fb4 h1:66lzN78lwccK+BPztRgBiWCYzhlerQEVOh2oeBksu5I=
github.com/codeskyblue/openid-go v0.0.0-20160923065855-0d30842b2fb4/go.mod h1:K/hSCtAHvnE9aM+LsYgVmgzPNFuWFdx6i9t6/3jNrZQ=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fork2fix/go-plist v0.0.0-20181126021357-36960be5e636 h1:ESUdS2eb8LyDQfboYyFBwAL+rqYhnTZ15ntw8BLsd9g=
github.com/fork2fix/go-plist v0.0.0-20181126021357-36960be5e636/go.mod h1:v6KRhgoO1QKamoeuZ7yHqZIP8p6j9k41Tb0jCyOEmr4=
github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d h1:lBXNCxVENCipq4D1Is42JVOP4eQjlB8TQ6H69Yx5J9Q=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/handlers v1.4.0 h1:XulKRWSQK5uChr4pEgSE4Tc/OcmnU9GJuSwdog/tZsA=
github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/shogo82148/androidbinary v0.0.0-20180627093851-01c4bfa8b3b5 h1:bXRaUWl3Afe3F9YR5NU1U3UB5zjCHlu4im5p3J/LUYk=
github.com/shogo82148/androidbinary v0.0.0-20180627093851-01c4bfa8b3b5/go.mod h1:05AjXWPWLdTIl9+REKhSmTeoJ6Wz5e9ir0Q0NRxCIKo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
howett.net/plist v0.0.0-20201203080718-1454fab16a06 h1:QDxUo/w2COstK1wIBYpzQlHX/NqaQTcf9jyz347nI58=
howett.net/plist v0.0.0-20201203080718-1454fab16a06/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=

View File

@@ -10,13 +10,13 @@ import (
"io/ioutil"
"log"
"mime"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"regexp"
@@ -42,71 +42,64 @@ type IndexFileItem struct {
Info os.FileInfo
}
type Directory struct {
size map[string]int64
mutex *sync.RWMutex
}
type HTTPStaticServer struct {
Root string
Prefix string
Upload bool
Delete bool
Title string
Theme string
PlistProxy string
GoogleTrackerID string
AuthType string
DeepPathMaxDepth int
NoIndex bool
Root string
Upload bool
Delete bool
Title string
Theme string
PlistProxy string
GoogleTrackerID string
AuthType string
DBModel Model
indexes []IndexFileItem
m *mux.Router
bufPool sync.Pool // use sync.Pool caching buf to reduce gc ratio
}
func NewHTTPStaticServer(root string, noIndex bool) *HTTPStaticServer {
// if root == "" {
// root = "./"
// }
// root = filepath.ToSlash(root)
root = filepath.ToSlash(filepath.Clean(root))
func NewHTTPStaticServer(root string, dbmodel Model) *HTTPStaticServer {
if root == "" {
root = "./"
}
root = filepath.ToSlash(root)
if !strings.HasSuffix(root, "/") {
root = root + "/"
}
log.Printf("root path: %s\n", root)
m := mux.NewRouter()
s := &HTTPStaticServer{
Root: root,
Theme: "black",
m: m,
bufPool: sync.Pool{
New: func() interface{} { return make([]byte, 32*1024) },
},
NoIndex: noIndex,
Root: root,
Theme: "black",
m: m,
DBModel: dbmodel,
}
if !noIndex {
go func() {
time.Sleep(1 * time.Second)
for {
startTime := time.Now()
log.Println("Started making search index")
s.makeIndex()
log.Printf("Completed search index in %v", time.Since(startTime))
//time.Sleep(time.Second * 1)
time.Sleep(time.Minute * 10)
}
}()
}
go func() {
time.Sleep(1 * time.Second)
for {
startTime := time.Now()
log.Println("Started making search index")
s.makeIndex()
log.Printf("Completed search index in %v", time.Since(startTime))
//time.Sleep(time.Second * 1)
time.Sleep(time.Minute * 10)
}
}()
m.HandleFunc("/-/status", s.hStatus)
m.HandleFunc("/-/zip/{path:.*}", s.hZip)
m.HandleFunc("/-/unzip/{zip_path:.*}/-/{path:.*}", s.hUnzip)
m.HandleFunc("/-/json/{path:.*}", s.hJSONList)
// routers for Apple *.ipa
m.HandleFunc("/-/ipa/plist/{path:.*}", s.hPlist)
m.HandleFunc("/-/ipa/link/{path:.*}", s.hIpaLink)
m.HandleFunc("/-/video-player/{path:.*}", s.hVideoPlayer)
// TODO: /ipa/info
m.HandleFunc("/-/info/{path:.*}", s.hInfo)
m.HandleFunc("/-/mkdir/{path:.*}", s.hMkdir)
m.HandleFunc("/{path:.*}", s.hIndex).Methods("GET", "HEAD")
m.HandleFunc("/{path:.*}", s.hUploadOrMkdir).Methods("POST")
m.HandleFunc("/{path:.*}", s.hUpload).Methods("POST")
m.HandleFunc("/{path:.*}", s.hDelete).Methods("DELETE")
return s
}
@@ -115,48 +108,18 @@ func (s *HTTPStaticServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.m.ServeHTTP(w, r)
}
// Return real path with Seperator(/)
func (s *HTTPStaticServer) getRealPath(r *http.Request) string {
path := mux.Vars(r)["path"]
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
path = filepath.Clean(path) // prevent .. for safe issues
relativePath, err := filepath.Rel(s.Prefix, path)
if err != nil {
relativePath = path
}
realPath := filepath.Join(s.Root, relativePath)
return filepath.ToSlash(realPath)
}
func (s *HTTPStaticServer) hIndex(w http.ResponseWriter, r *http.Request) {
path := mux.Vars(r)["path"]
realPath := s.getRealPath(r)
if r.FormValue("json") == "true" {
s.hJSONList(w, r)
return
}
if r.FormValue("op") == "info" {
s.hInfo(w, r)
return
}
if r.FormValue("op") == "archive" {
s.hZip(w, r)
return
}
log.Println("GET", path, realPath)
if r.FormValue("raw") == "false" || isDir(realPath) {
relPath := filepath.Join(s.Root, path)
log.Println("GET", path, relPath)
if r.FormValue("raw") == "false" || isDir(relPath) {
if r.Method == "HEAD" {
return
}
renderHTML(w, "assets/index.html", s)
renderHTML(w, "index.html", s)
} else {
if filepath.Base(path) == YAMLCONF {
auth := s.readAccessConf(realPath)
auth := s.readAccessConf(path)
if !auth.Delete {
http.Error(w, "Security warning, not allowed to read", http.StatusForbidden)
return
@@ -164,64 +127,67 @@ func (s *HTTPStaticServer) hIndex(w http.ResponseWriter, r *http.Request) {
}
if r.FormValue("download") == "true" {
w.Header().Set("Content-Disposition", "attachment; filename="+strconv.Quote(filepath.Base(path)))
DldIncre(s.DBModel, r.RemoteAddr, path)
}
http.ServeFile(w, r, realPath)
http.ServeFile(w, r, relPath)
}
}
func (s *HTTPStaticServer) hDelete(w http.ResponseWriter, req *http.Request) {
func (s *HTTPStaticServer) hStatus(w http.ResponseWriter, r *http.Request) {
data, _ := json.MarshalIndent(s, "", " ")
w.Header().Set("Content-Type", "application/json")
w.Write(data)
}
func (s *HTTPStaticServer) hMkdir(w http.ResponseWriter, req *http.Request) {
path := mux.Vars(req)["path"]
realPath := s.getRealPath(req)
// path = filepath.Clean(path) // for safe reason, prevent path contain ..
auth := s.readAccessConf(realPath)
auth := s.readAccessConf(path)
if !auth.canDelete(req) {
http.Error(w, "Delete forbidden", http.StatusForbidden)
http.Error(w, "Mkdir forbidden", http.StatusForbidden)
return
}
// TODO: path safe check
err := os.RemoveAll(realPath)
name := req.FormValue("name")
if err := checkFilename(name); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
err := os.Mkdir(filepath.Join(s.Root, path, name), 0755)
if err != nil {
pathErr, ok := err.(*os.PathError)
if ok {
http.Error(w, pathErr.Op+" "+path+": "+pathErr.Err.Error(), 500)
} else {
http.Error(w, err.Error(), 500)
}
http.Error(w, err.Error(), 500)
return
}
w.Write([]byte("Success"))
}
func (s *HTTPStaticServer) hUploadOrMkdir(w http.ResponseWriter, req *http.Request) {
dirpath := s.getRealPath(req)
func (s *HTTPStaticServer) hDelete(w http.ResponseWriter, req *http.Request) {
// only can delete file now
path := mux.Vars(req)["path"]
auth := s.readAccessConf(path)
log.Printf("%#v", auth)
if !auth.canDelete(req) {
http.Error(w, "Delete forbidden", http.StatusForbidden)
return
}
err := os.Remove(filepath.Join(s.Root, path))
if err != nil {
http.Error(w, err.Error(), 500)
return
}
w.Write([]byte("Success"))
}
func (s *HTTPStaticServer) hUpload(w http.ResponseWriter, req *http.Request) {
path := mux.Vars(req)["path"]
dirpath := filepath.Join(s.Root, path)
// check auth
auth := s.readAccessConf(dirpath)
auth := s.readAccessConf(path)
if !auth.canUpload(req) {
http.Error(w, "Upload forbidden", http.StatusForbidden)
return
}
file, header, err := req.FormFile("file")
if _, err := os.Stat(dirpath); os.IsNotExist(err) {
if err := os.MkdirAll(dirpath, os.ModePerm); err != nil {
log.Println("Create directory:", err)
http.Error(w, "Directory create "+err.Error(), http.StatusInternalServerError)
return
}
}
if file == nil { // only mkdir
w.Header().Set("Content-Type", "application/json;charset=utf-8")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"destination": dirpath,
})
return
}
if err != nil {
log.Println("Parse form file:", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -242,54 +208,19 @@ func (s *HTTPStaticServer) hUploadOrMkdir(w http.ResponseWriter, req *http.Reque
}
dstPath := filepath.Join(dirpath, filename)
// Large file (>32MB) will store in tmp directory
// The quickest operation is call os.Move instead of os.Copy
// Note: it seems not working well, os.Rename might be failed
var copyErr error
// if osFile, ok := file.(*os.File); ok && fileExists(osFile.Name()) {
// tmpUploadPath := osFile.Name()
// osFile.Close() // Windows can not rename opened file
// log.Printf("Move %s -> %s", tmpUploadPath, dstPath)
// copyErr = os.Rename(tmpUploadPath, dstPath)
// } else {
dst, err := os.Create(dstPath)
if err != nil {
log.Println("Create file:", err)
http.Error(w, "File create "+err.Error(), http.StatusInternalServerError)
return
}
// Note: very large size file might cause poor performance
// _, copyErr = io.Copy(dst, file)
buf := s.bufPool.Get().([]byte)
defer s.bufPool.Put(buf)
_, copyErr = io.CopyBuffer(dst, file, buf)
dst.Close()
// }
if copyErr != nil {
defer dst.Close()
if _, err := io.Copy(dst, file); err != nil {
log.Println("Handle upload file:", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json;charset=utf-8")
if req.FormValue("unzip") == "true" {
err = unzipFile(dstPath, dirpath)
os.Remove(dstPath)
message := "success"
if err != nil {
message = err.Error()
}
json.NewEncoder(w).Encode(map[string]interface{}{
"success": err == nil,
"description": message,
})
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"destination": dstPath,
@@ -326,8 +257,11 @@ func parseApkInfo(path string) (ai *ApkInfo) {
func (s *HTTPStaticServer) hInfo(w http.ResponseWriter, r *http.Request) {
path := mux.Vars(r)["path"]
relPath := s.getRealPath(r)
relPath := filepath.Join(s.Root, path)
if !isFile(relPath) {
http.Error(w, "Not a file", 403)
return
}
fi, err := os.Stat(relPath)
if err != nil {
http.Error(w, err.Error(), 500)
@@ -346,8 +280,6 @@ func (s *HTTPStaticServer) hInfo(w http.ResponseWriter, r *http.Request) {
case ".apk":
fji.Type = "apk"
fji.Extra = parseApkInfo(relPath)
case "":
fji.Type = "dir"
default:
fji.Type = "text"
}
@@ -357,7 +289,9 @@ func (s *HTTPStaticServer) hInfo(w http.ResponseWriter, r *http.Request) {
}
func (s *HTTPStaticServer) hZip(w http.ResponseWriter, r *http.Request) {
CompressToZip(w, s.getRealPath(r))
path := mux.Vars(r)["path"]
DldIncre(s.DBModel, r.RemoteAddr, path)
CompressToZip(w, filepath.Join(s.Root, path))
}
func (s *HTTPStaticServer) hUnzip(w http.ResponseWriter, r *http.Request) {
@@ -374,9 +308,13 @@ func (s *HTTPStaticServer) hUnzip(w http.ResponseWriter, r *http.Request) {
}
}
func combineURL(r *http.Request, path string) *url.URL {
func genURLStr(r *http.Request, path string) *url.URL {
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
return &url.URL{
Scheme: r.URL.Scheme,
Scheme: scheme,
Host: r.Host,
Path: path,
}
@@ -389,7 +327,7 @@ func (s *HTTPStaticServer) hPlist(w http.ResponseWriter, r *http.Request) {
path = path[0:len(path)-6] + ".ipa"
}
relPath := s.getRealPath(r)
relPath := filepath.Join(s.Root, path)
plinfo, err := parseIPA(relPath)
if err != nil {
http.Error(w, err.Error(), 500)
@@ -415,11 +353,9 @@ func (s *HTTPStaticServer) hPlist(w http.ResponseWriter, r *http.Request) {
func (s *HTTPStaticServer) hIpaLink(w http.ResponseWriter, r *http.Request) {
path := mux.Vars(r)["path"]
var plistUrl string
if r.URL.Scheme == "https" {
plistUrl = combineURL(r, "/-/ipa/plist/"+path).String()
} else if s.PlistProxy != "" {
plistUrl := genURLStr(r, "/-/ipa/plist/"+path).String()
if r.TLS == nil {
// send plist to plistproxy and get a https link
httpPlistLink := "http://" + r.Host + "/-/ipa/plist/" + path
url, err := s.genPlistLink(httpPlistLink)
if err != nil {
@@ -427,14 +363,11 @@ func (s *HTTPStaticServer) hIpaLink(w http.ResponseWriter, r *http.Request) {
return
}
plistUrl = url
} else {
http.Error(w, "500: Server should be https:// or provide valid plistproxy", 500)
return
}
w.Header().Set("Content-Type", "text/html")
log.Println("PlistURL:", plistUrl)
renderHTML(w, "assets/ipa-install.html", map[string]string{
renderHTML(w, "ipa-install.html", map[string]string{
"Name": filepath.Base(path),
"PlistLink": plistUrl,
})
@@ -469,7 +402,8 @@ func (s *HTTPStaticServer) genPlistLink(httpPlistLink string) (plistUrl string,
}
func (s *HTTPStaticServer) hFileOrDirectory(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, s.getRealPath(r))
path := mux.Vars(r)["path"]
http.ServeFile(w, r, filepath.Join(s.Root, path))
}
type HTTPFileInfo struct {
@@ -478,6 +412,7 @@ type HTTPFileInfo struct {
Type string `json:"type"`
Size int64 `json:"size"`
ModTime int64 `json:"mtime"`
DldCnt int `json:"dldcnt"`
}
type AccessTable struct {
@@ -572,12 +507,11 @@ func (c *AccessConf) canUpload(r *http.Request) bool {
func (s *HTTPStaticServer) hJSONList(w http.ResponseWriter, r *http.Request) {
requestPath := mux.Vars(r)["path"]
realPath := s.getRealPath(r)
localPath := filepath.Join(s.Root, requestPath)
search := r.FormValue("search")
auth := s.readAccessConf(realPath)
auth := s.readAccessConf(requestPath)
auth.Upload = auth.canUpload(r)
auth.Delete = auth.canDelete(r)
maxDepth := s.DeepPathMaxDepth
// path string -> info os.FileInfo
fileInfoMap := make(map[string]os.FileInfo, 0)
@@ -593,7 +527,7 @@ func (s *HTTPStaticServer) hJSONList(w http.ResponseWriter, r *http.Request) {
}
}
} else {
infos, err := ioutil.ReadDir(realPath)
infos, err := ioutil.ReadDir(localPath)
if err != nil {
http.Error(w, err.Error(), 500)
return
@@ -613,6 +547,7 @@ func (s *HTTPStaticServer) hJSONList(w http.ResponseWriter, r *http.Request) {
Name: info.Name(),
Path: path,
ModTime: info.ModTime().UnixNano() / 1e6,
DldCnt: (*s.DBModel.Data)[path],
}
if search != "" {
name, err := filepath.Rel(requestPath, path)
@@ -622,7 +557,7 @@ func (s *HTTPStaticServer) hJSONList(w http.ResponseWriter, r *http.Request) {
lr.Name = filepath.ToSlash(name) // fix for windows
}
if info.IsDir() {
name := deepPath(realPath, info.Name(), maxDepth)
name := deepPath(localPath, info.Name())
lr.Name = name
lr.Path = filepath.Join(filepath.Dir(path), name)
lr.Type = "dir"
@@ -642,7 +577,7 @@ func (s *HTTPStaticServer) hJSONList(w http.ResponseWriter, r *http.Request) {
w.Write(data)
}
var dirInfoSize = Directory{size: make(map[string]int64), mutex: &sync.RWMutex{}}
var dirSizeMap = make(map[string]int64)
func (s *HTTPStaticServer) makeIndex() error {
var indexes = make([]IndexFileItem, 0)
@@ -662,28 +597,21 @@ func (s *HTTPStaticServer) makeIndex() error {
return nil
})
s.indexes = indexes
dirSizeMap = make(map[string]int64)
return err
}
func (s *HTTPStaticServer) historyDirSize(dir string) int64 {
dirInfoSize.mutex.RLock()
size, ok := dirInfoSize.size[dir]
dirInfoSize.mutex.RUnlock()
if ok {
var size int64
if size, ok := dirSizeMap[dir]; ok {
return size
}
for _, fitem := range s.indexes {
if filepath.HasPrefix(fitem.Path, dir) {
size += fitem.Info.Size()
}
}
dirInfoSize.mutex.Lock()
dirInfoSize.size[dir] = size
dirInfoSize.mutex.Unlock()
dirSizeMap[dir] = size
return size
}
@@ -720,19 +648,19 @@ func (s *HTTPStaticServer) defaultAccessConf() AccessConf {
}
}
func (s *HTTPStaticServer) readAccessConf(realPath string) (ac AccessConf) {
relativePath, err := filepath.Rel(s.Root, realPath)
if err != nil || relativePath == "." || relativePath == "" { // actually relativePath is always "." if root == realPath
func (s *HTTPStaticServer) readAccessConf(requestPath string) (ac AccessConf) {
requestPath = filepath.Clean(requestPath)
if requestPath == "/" || requestPath == "" || requestPath == "." {
ac = s.defaultAccessConf()
realPath = s.Root
} else {
parentPath := filepath.Dir(realPath)
parentPath := filepath.Dir(requestPath)
ac = s.readAccessConf(parentPath)
}
if isFile(realPath) {
realPath = filepath.Dir(realPath)
relPath := filepath.Join(s.Root, requestPath)
if isFile(relPath) {
relPath = filepath.Dir(relPath)
}
cfgFile := filepath.Join(realPath, YAMLCONF)
cfgFile := filepath.Join(relPath, YAMLCONF)
data, err := ioutil.ReadFile(cfgFile)
if err != nil {
if os.IsNotExist(err) {
@@ -747,9 +675,11 @@ func (s *HTTPStaticServer) readAccessConf(realPath string) (ac AccessConf) {
return
}
func deepPath(basedir, name string, maxDepth int) string {
func deepPath(basedir, name string) string {
isDir := true
// loop max 5, incase of for loop not finished
for depth := 0; depth <= maxDepth; depth += 1 {
maxDepth := 5
for depth := 0; depth <= maxDepth && isDir; depth += 1 {
finfos, err := ioutil.ReadDir(filepath.Join(basedir, name))
if err != nil || len(finfos) != 1 {
break
@@ -763,6 +693,16 @@ func deepPath(basedir, name string, maxDepth int) string {
return name
}
func isFile(path string) bool {
info, err := os.Stat(path)
return err == nil && info.Mode().IsRegular()
}
func isDir(path string) bool {
info, err := os.Stat(path)
return err == nil && info.Mode().IsDir()
}
func assetsContent(name string) string {
fd, err := Assets.Open(name)
if err != nil {
@@ -801,7 +741,7 @@ var (
_tmpls = make(map[string]*template.Template)
)
func renderHTML(w http.ResponseWriter, name string, v interface{}) {
func executeTemplate(w http.ResponseWriter, name string, v interface{}) {
if t, ok := _tmpls[name]; ok {
t.Execute(w, v)
return
@@ -811,6 +751,16 @@ func renderHTML(w http.ResponseWriter, name string, v interface{}) {
t.Execute(w, v)
}
func renderHTML(w http.ResponseWriter, name string, v interface{}) {
if _, ok := Assets.(http.Dir); ok {
log.Println("Hot load", name)
t := template.Must(template.New(name).Funcs(funcMap).Delims("[[", "]]").Parse(assetsContent(name)))
t.Execute(w, v)
} else {
executeTemplate(w, name, v)
}
}
func checkFilename(name string) error {
if strings.ContainsAny(name, "\\/:*<>|") {
return errors.New("Name should not contains \\/:*<>|")
@@ -818,27 +768,9 @@ func checkFilename(name string) error {
return nil
}
func (s *HTTPStaticServer) hVideoPlayer(w http.ResponseWriter, r *http.Request) {
path := mux.Vars(r)["path"]
realPath := s.getRealPath(r)
extension := strings.ToLower(strings.TrimPrefix(filepath.Ext(path), "."))
if _, err := os.Stat(realPath); os.IsNotExist(err) {
http.Error(w, "File not found", http.StatusNotFound)
return
func DldIncre(model Model, rAddress string, path string) {
ip, _, _ := net.SplitHostPort(rAddress)
if DCounter.Validate(ip, path) {
model.Incre(path)
}
fileName := filepath.Base(path)
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
videoURL := fmt.Sprintf("%s://%s/%s", scheme, r.Host, path)
renderHTML(w, "assets/video-player.html", map[string]interface{}{
"FileName": fileName,
"VideoURL": videoURL,
"Extension": extension,
})
}

128
main.go
View File

@@ -2,8 +2,6 @@ package main
import (
"bytes"
"crypto/sha256"
"crypto/subtle"
"encoding/json"
"fmt"
"io/ioutil"
@@ -12,7 +10,6 @@ import (
"net/http"
"net/url"
"os"
"regexp"
"runtime"
"strconv"
"strings"
@@ -23,7 +20,7 @@ import (
"github.com/go-yaml/yaml"
"github.com/goji/httpauth"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
_ "github.com/shurcooL/vfsgen"
)
type Configure struct {
@@ -31,10 +28,10 @@ type Configure struct {
Addr string `yaml:"addr"`
Port int `yaml:"port"`
Root string `yaml:"root"`
Prefix string `yaml:"prefix"`
HTTPAuth string `yaml:"httpauth"`
Cert string `yaml:"cert"`
Key string `yaml:"key"`
Cors bool `yaml:"cors"`
Theme string `yaml:"theme"`
XHeaders bool `yaml:"xheaders"`
Upload bool `yaml:"upload"`
@@ -44,14 +41,12 @@ type Configure struct {
Debug bool `yaml:"debug"`
GoogleTrackerID string `yaml:"google-tracker-id"`
Auth struct {
Type string `yaml:"type"` // openid|http|github
OpenID string `yaml:"openid"`
HTTP []string `yaml:"http"`
ID string `yaml:"id"` // for oauth2
Secret string `yaml:"secret"` // for oauth2
Type string `yaml:"type"` // openid|http|github
OpenID string `yaml:"openid"`
HTTP string `yaml:"http"`
ID string `yaml:"id"` // for oauth2
Secret string `yaml:"secret"` // for oauth2
} `yaml:"auth"`
DeepPathMaxDepth int `yaml:"deep-path-max-depth"`
NoIndex bool `yaml:"no-index"`
}
type httpLogger struct{}
@@ -102,31 +97,27 @@ func parseFlags() error {
gcfg.Auth.OpenID = defaultOpenID
gcfg.GoogleTrackerID = "UA-81205425-2"
gcfg.Title = "Go HTTP File Server"
gcfg.DeepPathMaxDepth = 5
gcfg.NoIndex = false
kingpin.HelpFlag.Short('h')
kingpin.Version(versionMessage())
kingpin.Flag("conf", "config file path, yaml format").FileVar(&gcfg.Conf)
kingpin.Flag("root", "root directory, default ./").Short('r').StringVar(&gcfg.Root)
kingpin.Flag("prefix", "url prefix, eg /foo").StringVar(&gcfg.Prefix)
kingpin.Flag("port", "listen port, default 8000").IntVar(&gcfg.Port)
kingpin.Flag("addr", "listen address, eg 127.0.0.1:8000").Short('a').StringVar(&gcfg.Addr)
kingpin.Flag("cert", "tls cert.pem path").StringVar(&gcfg.Cert)
kingpin.Flag("key", "tls key.pem path").StringVar(&gcfg.Key)
kingpin.Flag("auth-type", "Auth type <http|openid>").StringVar(&gcfg.Auth.Type)
kingpin.Flag("auth-http", "HTTP basic auth (ex: user:pass)").StringsVar(&gcfg.Auth.HTTP)
kingpin.Flag("auth-http", "HTTP basic auth (ex: user:pass)").StringVar(&gcfg.Auth.HTTP)
kingpin.Flag("auth-openid", "OpenID auth identity url").StringVar(&gcfg.Auth.OpenID)
kingpin.Flag("theme", "web theme, one of <black|green>").StringVar(&gcfg.Theme)
kingpin.Flag("upload", "enable upload support").BoolVar(&gcfg.Upload)
kingpin.Flag("delete", "enable delete support").BoolVar(&gcfg.Delete)
kingpin.Flag("xheaders", "used when behide nginx").BoolVar(&gcfg.XHeaders)
kingpin.Flag("cors", "enable cross-site HTTP request").BoolVar(&gcfg.Cors)
kingpin.Flag("debug", "enable debug mode").BoolVar(&gcfg.Debug)
kingpin.Flag("plistproxy", "plist proxy when server is not https").Short('p').StringVar(&gcfg.PlistProxy)
kingpin.Flag("title", "server title").StringVar(&gcfg.Title)
kingpin.Flag("google-tracker-id", "set to empty to disable it").StringVar(&gcfg.GoogleTrackerID)
kingpin.Flag("deep-path-max-depth", "set to -1 to not combine dirs").IntVar(&gcfg.DeepPathMaxDepth)
kingpin.Flag("no-index", "disable indexing").BoolVar(&gcfg.NoIndex)
kingpin.Parse() // first parse conf
@@ -143,52 +134,6 @@ func parseFlags() error {
return nil
}
func fixPrefix(prefix string) string {
prefix = regexp.MustCompile(`/*$`).ReplaceAllString(prefix, "")
if !strings.HasPrefix(prefix, "/") {
prefix = "/" + prefix
}
if prefix == "/" {
prefix = ""
}
return prefix
}
func cors(next http.Handler) http.Handler {
// access control and CORS middleware
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "*")
w.Header().Set("Access-Control-Allow-Headers", "*")
if r.Method == "OPTIONS" {
return
}
next.ServeHTTP(w, r)
})
}
func multiBasicAuth(auths []string) func(http.Handler) http.Handler {
userPassMap := make(map[string]string)
for _, auth := range auths {
userpass := strings.SplitN(auth, ":", 2)
if len(userpass) == 2 {
userPassMap[userpass[0]] = userpass[1]
}
}
return httpauth.BasicAuth(httpauth.AuthOptions{
Realm: "Restricted",
AuthFunc: func(user, pass string, request *http.Request) bool {
password, ok := userPassMap[user]
if !ok {
return false
}
givenPass := sha256.Sum256([]byte(pass))
requiredPass := sha256.Sum256([]byte(password))
return subtle.ConstantTimeCompare(givenPass[:], requiredPass[:]) == 1
},
})
}
func main() {
if err := parseFlags(); err != nil {
log.Fatal(err)
@@ -199,21 +144,14 @@ func main() {
}
log.SetFlags(log.Lshortfile | log.LstdFlags)
// make sure prefix matches: ^/.*[^/]$
gcfg.Prefix = fixPrefix(gcfg.Prefix)
if gcfg.Prefix != "" {
log.Printf("url prefix: %s", gcfg.Prefix)
}
ss := NewHTTPStaticServer(gcfg.Root, gcfg.NoIndex)
ss.Prefix = gcfg.Prefix
DBModel.DBRead("ghs-stats.json")
ss := NewHTTPStaticServer(gcfg.Root, DBModel) // FIXME(ssx): should put DBModel inside HTTPStaticServer
ss.Theme = gcfg.Theme
ss.Title = gcfg.Title
ss.GoogleTrackerID = gcfg.GoogleTrackerID
ss.Upload = gcfg.Upload
ss.Delete = gcfg.Delete
ss.AuthType = gcfg.Auth.Type
ss.DeepPathMaxDepth = gcfg.DeepPathMaxDepth
if gcfg.PlistProxy != "" {
u, err := url.Parse(gcfg.PlistProxy)
@@ -223,53 +161,42 @@ func main() {
u.Scheme = "https"
ss.PlistProxy = u.String()
}
if ss.PlistProxy != "" {
log.Printf("plistproxy: %s", strconv.Quote(ss.PlistProxy))
}
var hdlr http.Handler = ss
hdlr = accesslog.NewLoggingHandler(hdlr, logger)
// HTTP Basic Authentication
userpass := strings.SplitN(gcfg.Auth.HTTP, ":", 2)
switch gcfg.Auth.Type {
case "http":
hdlr = multiBasicAuth(gcfg.Auth.HTTP)(hdlr)
if len(userpass) == 2 {
user, pass := userpass[0], userpass[1]
hdlr = httpauth.SimpleBasicAuth(user, pass)(hdlr)
}
case "openid":
handleOpenID(gcfg.Auth.OpenID, false) // FIXME(ssx): set secure default to false
// case "github":
// handleOAuth2ID(gcfg.Auth.Type, gcfg.Auth.ID, gcfg.Auth.Secret) // FIXME(ssx): set secure default to false
case "oauth2-proxy":
handleOauth2()
}
// CORS
hdlr = cors(hdlr)
if gcfg.Cors {
hdlr = handlers.CORS()(hdlr)
}
if gcfg.XHeaders {
hdlr = handlers.ProxyHeaders(hdlr)
}
mainRouter := mux.NewRouter()
router := mainRouter
if gcfg.Prefix != "" {
router = mainRouter.PathPrefix(gcfg.Prefix).Subrouter()
mainRouter.Handle(gcfg.Prefix, hdlr)
mainRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, gcfg.Prefix, http.StatusTemporaryRedirect)
})
}
router.PathPrefix("/-/assets/").Handler(http.StripPrefix(gcfg.Prefix+"/-/", http.FileServer(Assets)))
router.HandleFunc("/-/sysinfo", func(w http.ResponseWriter, r *http.Request) {
http.Handle("/", hdlr)
http.Handle("/-/assets/", http.StripPrefix("/-/assets/", http.FileServer(Assets)))
http.HandleFunc("/-/sysinfo", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
data, _ := json.Marshal(map[string]interface{}{
"version": VERSION,
})
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(data)))
w.Write(data)
})
router.PathPrefix("/").Handler(hdlr)
if gcfg.Addr == "" {
gcfg.Addr = fmt.Sprintf(":%d", gcfg.Port)
@@ -280,16 +207,11 @@ func main() {
_, port, _ := net.SplitHostPort(gcfg.Addr)
log.Printf("listening on %s, local address http://%s:%s\n", strconv.Quote(gcfg.Addr), getLocalIP(), port)
srv := &http.Server{
Handler: mainRouter,
Addr: gcfg.Addr,
}
var err error
if gcfg.Key != "" && gcfg.Cert != "" {
err = srv.ListenAndServeTLS(gcfg.Cert, gcfg.Key)
err = http.ListenAndServeTLS(gcfg.Addr, gcfg.Cert, gcfg.Key, nil)
} else {
err = srv.ListenAndServe()
err = http.ListenAndServe(gcfg.Addr, nil)
}
log.Fatal(err)
}

View File

@@ -1,27 +0,0 @@
package main
import (
"encoding/json"
"net/http"
"net/url"
)
func handleOauth2() {
http.HandleFunc("/-/user", func(w http.ResponseWriter, r *http.Request) {
fullNameMap, _ := url.ParseQuery(r.Header.Get("X-Auth-Request-Fullname"))
var fullName string
for k := range fullNameMap {
fullName = k
break
}
user := &UserInfo{
Email: r.Header.Get("X-Auth-Request-Email"),
Name: fullName,
NickName: r.Header.Get("X-Auth-Request-User"),
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
data, _ := json.Marshal(user)
w.Write(data)
})
}

View File

@@ -1 +0,0 @@
document.write("Hello world!")

View File

@@ -3,8 +3,6 @@ package main
import (
"net"
"net/http"
"os"
"path/filepath"
"strings"
)
@@ -73,26 +71,3 @@ func getLocalIP() string {
}
return ""
}
func fileExists(path string) bool {
info, err := os.Stat(path)
if err != nil {
return false
}
return !info.IsDir()
}
// Convert path to normal paths
func cleanPath(path string) string {
return filepath.ToSlash(filepath.Clean(path))
}
func isFile(path string) bool {
info, err := os.Stat(path)
return err == nil && info.Mode().IsRegular()
}
func isDir(path string) bool {
info, err := os.Stat(path)
return err == nil && info.Mode().IsDir()
}

View File

@@ -25,25 +25,3 @@ func TestSublimeContains(t *testing.T) {
}
}
}
func TestCleanPath(t *testing.T) {
tests := []struct {
orig string
expect string
}{
// {"C:\\hello", "C:/hello"}, // Only works in windows
{"", "."},
{"//../foo", "/foo"},
{"/../../", "/"},
{"/hello/world/..", "/hello"},
{"/..", "/"},
{"/foo/..", "/"},
{"/-/foo", "/-/foo"},
}
for _, v := range tests {
res := cleanPath(v.orig)
if res != v.expect {
t.Fatalf("Clean path(%v) expect(%v) but got(%v)", v.orig, v.expect, res)
}
}
}

56
zip.go
View File

@@ -14,7 +14,6 @@ import (
"strings"
dkignore "github.com/codeskyblue/dockerignore"
"golang.org/x/text/encoding/simplifiedchinese"
)
type Zip struct {
@@ -131,58 +130,3 @@ func ExtractFromZip(zipFile, path string, w io.Writer) (err error) {
}
return fmt.Errorf("File %s not found", strconv.Quote(path))
}
func unzipFile(filename, dest string) error {
zr, err := zip.OpenReader(filename)
if err != nil {
return err
}
defer zr.Close()
if dest == "" {
dest = filepath.Dir(filename)
}
for _, f := range zr.File {
rc, err := f.Open()
if err != nil {
return err
}
defer rc.Close()
// ignore .ghs.yml
filename := sanitizedName(f.Name)
if filepath.Base(filename) == ".ghs.yml" {
continue
}
fpath := filepath.Join(dest, filename)
// filename maybe GBK or UTF-8
// Ref: https://studygolang.com/articles/3114
if f.Flags&(1<<11) == 0 { // GBK
tr := simplifiedchinese.GB18030.NewDecoder()
fpathUtf8, err := tr.String(fpath)
if err == nil {
fpath = fpathUtf8
}
}
if f.FileInfo().IsDir() {
os.MkdirAll(fpath, os.ModePerm)
continue
}
os.MkdirAll(filepath.Dir(fpath), os.ModePerm)
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
_, err = io.Copy(outFile, rc)
outFile.Close()
if err != nil {
return err
}
}
return nil
}

View File

@@ -3,18 +3,13 @@ package main
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
)
func TestExtractFromZip(t *testing.T) {
buf := bytes.NewBuffer(nil)
err := ExtractFromZip("testdata/test.zip", "**/foo.txt", buf)
assert.Nil(t, err)
if err != nil {
t.Fatal(err)
}
t.Log("Content: " + buf.String())
}
//func TestUnzipTo(t *testing.T){
// err := unzipFile("testdata.zip", "./tmp")
// assert.Nil(t, err)
//}