Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6829892be4 | ||
|
|
26545513de | ||
|
|
342d9cc14c | ||
|
|
ed85a9687b | ||
|
|
fe8dcd76c1 | ||
|
|
230bc3d182 | ||
|
|
28a90b90dd | ||
|
|
e607da8ec2 | ||
|
|
e47bdd1272 | ||
|
|
ff821dcb63 | ||
|
|
8887f4ef69 | ||
|
|
bd1a35541a | ||
|
|
56045c6c8e | ||
|
|
b5bd6779f9 | ||
|
|
8665999fc0 | ||
|
|
677c5a7cec | ||
|
|
41981a3d8a | ||
|
|
47789eec68 | ||
|
|
4c880193ce | ||
|
|
86aeecfac2 | ||
|
|
e99a12b6c3 | ||
|
|
e8af97872c | ||
|
|
d7590729f1 | ||
|
|
5a7724d542 | ||
|
|
f7063651c2 | ||
|
|
42d1e077c9 | ||
|
|
1258ac759f | ||
|
|
a60a9c4811 | ||
|
|
b6200a88c0 | ||
|
|
1618440f4f | ||
|
|
f676d25787 | ||
|
|
e24ff056f7 | ||
|
|
833443dfaf | ||
|
|
b6f8563d18 | ||
|
|
20ed231c53 | ||
|
|
b4470cf4bd | ||
|
|
bd4e9250c0 | ||
|
|
00940603ca | ||
|
|
621dc6c22f | ||
|
|
5367b9ea94 | ||
|
|
8e275e27be | ||
|
|
56e184748f | ||
|
|
b40bf8101f | ||
|
|
70e364c57b | ||
|
|
98b09cb27f | ||
|
|
aad95ae813 | ||
|
|
2bd7ec5cd0 | ||
|
|
85b2bd5dc4 | ||
|
|
fdec6aa9ac | ||
|
|
eaa82b8605 | ||
|
|
f4bdbeee73 | ||
|
|
905412e91b | ||
|
|
9318d19383 | ||
|
|
d62e765ae7 | ||
|
|
097ff92f17 | ||
|
|
fe28075b35 | ||
|
|
3339651d88 | ||
|
|
c8d67edf2c | ||
|
|
41ba422675 | ||
|
|
f9f3142543 | ||
|
|
9442e00997 | ||
|
|
1bbc103672 | ||
|
|
a16f689e99 | ||
|
|
6d5aae4dde | ||
|
|
9f9d518689 | ||
|
|
23441ea0c2 | ||
|
|
d22065f7e0 | ||
|
|
8e3ddf6f3c | ||
|
|
0fdd7d5906 | ||
|
|
e0ced3f803 | ||
|
|
261a04d83f | ||
|
|
d0fd2cc458 | ||
|
|
7056ab85f8 | ||
|
|
84d280a9a4 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -29,3 +29,5 @@ bindata_assetfs.go
|
||||
assets_vfsdata.go
|
||||
*.un~
|
||||
*.swp
|
||||
|
||||
dist/
|
||||
|
||||
@@ -1,44 +1,23 @@
|
||||
project_name: gohttpserver
|
||||
release:
|
||||
github:
|
||||
owner: codeskyblue
|
||||
name: gohttpserver
|
||||
brew:
|
||||
github:
|
||||
owner: codeskyblue
|
||||
name: homebrew-tap
|
||||
homepage: https://github.com/codeskyblue/gohttpserver
|
||||
builds:
|
||||
- goos:
|
||||
- linux
|
||||
- darwin
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- "386"
|
||||
goarm:
|
||||
- "6"
|
||||
main: .
|
||||
ldflags: -s -w -X main.VERSION={{.Version}}
|
||||
flags: -tags vfs
|
||||
binary: gohttpserver
|
||||
before:
|
||||
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'
|
||||
- go mod tidy
|
||||
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"
|
||||
brews:
|
||||
- tap:
|
||||
owner: codeskyblue
|
||||
name: homebrew-tap
|
||||
folder: Formula
|
||||
|
||||
13
.travis.yml
13
.travis.yml
@@ -1,9 +1,8 @@
|
||||
sudo: required
|
||||
services:
|
||||
- docker
|
||||
language: go
|
||||
go:
|
||||
- "1.11"
|
||||
- "1.18"
|
||||
env:
|
||||
- GO111MODULE=on
|
||||
script:
|
||||
@@ -19,8 +18,8 @@ deploy:
|
||||
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
|
||||
#- provider: script
|
||||
# skip_cleanup: true
|
||||
# script: bash docker/push_images && bash docker/push_manifest
|
||||
# on:
|
||||
# branch: master
|
||||
|
||||
102
README.md
102
README.md
@@ -1,5 +1,6 @@
|
||||
# gohttpserver
|
||||
[](https://travis-ci.org/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.
|
||||
@@ -12,7 +13,7 @@
|
||||
**Binaries** can be downloaded from [this repo releases](https://github.com/codeskyblue/gohttpserver/releases/)
|
||||
|
||||
## Requirements
|
||||
Tested with go-1.10, go-1.11
|
||||
Tested with go-1.16
|
||||
|
||||
## Screenshots
|
||||

|
||||
@@ -54,21 +55,26 @@ Tested with go-1.10, go-1.11
|
||||
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
|
||||
```
|
||||
go get -v github.com/codeskyblue/gohttpserver
|
||||
cd $GOPATH/src/github.com/codeskyblue/gohttpserver
|
||||
go build && ./gohttpserver
|
||||
```bash
|
||||
$ go install github.com/codeskyblue/gohttpserver@latest
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -77,13 +83,13 @@ 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 username:password
|
||||
```
|
||||
@@ -91,11 +97,18 @@ docker run -it --rm -p 8000:8000 -v $PWD:/app/public --name gohttpserver \
|
||||
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
|
||||
|
||||
@@ -109,6 +122,24 @@ docker run -it --rm -p 8000:8000 -v $PWD:/app/public --name gohttpserver \
|
||||
$ 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
|
||||
@@ -165,8 +196,8 @@ accessTables:
|
||||
### ipa plist proxy
|
||||
This is used for server on which https is enabled. default use <https://plistproxy.herokuapp.com/plist>
|
||||
|
||||
```
|
||||
./gohttpserver --plistproxy=https://someproxyhost.com/
|
||||
```bash
|
||||
$ gohttpserver --plistproxy=https://someproxyhost.com/
|
||||
```
|
||||
|
||||
Test if proxy works:
|
||||
@@ -180,6 +211,8 @@ $ 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`
|
||||
|
||||
@@ -195,6 +228,13 @@ $ 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
|
||||
@@ -218,8 +258,39 @@ 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)
|
||||
|
||||
@@ -235,14 +306,13 @@ 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 generate .
|
||||
go build -tags vfs
|
||||
$ go build
|
||||
```
|
||||
|
||||
Theme are defined in [assets/themes](assets/themes) directory. Now only two themes are available, "black" and "green".
|
||||
@@ -262,7 +332,7 @@ Theme are defined in [assets/themes](assets/themes) directory. Now only two them
|
||||
|
||||
**Go Libraries**
|
||||
|
||||
* [vfsgen](https://github.com/shurcooL/vfsgen)
|
||||
* [vfsgen](https://github.com/shurcooL/vfsgen) Not using now
|
||||
* [go-bindata-assetfs](https://github.com/elazarl/go-bindata-assetfs) Not using now
|
||||
* <http://www.gorillatoolkit.org/pkg/handlers>
|
||||
|
||||
|
||||
12
assets.go
Normal file
12
assets.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//go:embed assets
|
||||
var assetsFS embed.FS
|
||||
|
||||
// Assets contains project assets.
|
||||
var Assets = http.FS(assetsFS)
|
||||
@@ -4,15 +4,16 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<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">
|
||||
<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">
|
||||
</head>
|
||||
|
||||
<body id="app">
|
||||
@@ -26,7 +27,7 @@
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="/">[[.Title]]</a>
|
||||
<a class="navbar-brand" href="[[.Prefix]]/">[[.Title]]</a>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-2">
|
||||
<ul class="nav navbar-nav">
|
||||
@@ -49,13 +50,26 @@
|
||||
</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>
|
||||
<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="button">
|
||||
<button class="btn btn-default" type="submit">
|
||||
<span class="glyphicon glyphicon-search"></span>
|
||||
</button>
|
||||
</span>
|
||||
@@ -115,7 +129,7 @@
|
||||
<tbody>
|
||||
<tr v-for="f in computedFiles">
|
||||
<td>
|
||||
<a v-on:click='clickFileOrDir(f, $event)' href="/{{f.path + (f.type == 'dir' ? '' : '')}}">
|
||||
<a v-on:click='clickFileOrDir(f, $event)' href="{{getEncodePath(f.name)}}">
|
||||
<!-- ?raw=false -->
|
||||
<i style="padding-right: 0.5em" class="fa" v-bind:class='genFileClass(f)'></i> {{f.name}}
|
||||
</a>
|
||||
@@ -128,16 +142,19 @@
|
||||
<td class="hidden-xs">{{formatTime(f.mtime)}}</td>
|
||||
<td style="text-align: left">
|
||||
<template v-if="f.type == 'dir'">
|
||||
<a class="btn btn-default btn-xs" href="/-/zip/{{f.path}}">
|
||||
<a class="btn btn-default btn-xs" href="{{getEncodePath(f.name)}}/?op=archive">
|
||||
<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="/{{f.path}}?download=true">
|
||||
<a class="btn btn-default btn-xs hidden-xs" href="{{genDownloadURL(f)}}">
|
||||
<span class="hidden-xs">Download</span>
|
||||
<span class="glyphicon glyphicon-download-alt"></span>
|
||||
</a>
|
||||
@@ -239,24 +256,31 @@
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<div id="footer" class="pull-right" style="margin: 2em 1em">
|
||||
<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
|
||||
<a href="https://github.com/codeskyblue/gohttpserver">gohttpserver ({{version}})</a>, by <a href="https://github.com/codeskyblue">codeskyblue</a>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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> -->
|
||||
<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>
|
||||
[[if .GoogleTrackerID ]]
|
||||
<script>
|
||||
(function (i, s, o, g, r, a, m) {
|
||||
@@ -276,4 +300,4 @@
|
||||
</script> [[ end ]]
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -21,6 +21,21 @@ 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: {
|
||||
@@ -125,6 +140,9 @@ 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) {
|
||||
@@ -151,7 +169,7 @@ var vm = new Vue({
|
||||
if (!name) {
|
||||
parts.push(pathname);
|
||||
} else if (getExtention(name) == "ipa") {
|
||||
parts.push("/-/ipa/link", pathname, name);
|
||||
parts.push("/-/ipa/link", pathname, encodeURIComponent(name));
|
||||
} else {
|
||||
parts.push(pathname, name);
|
||||
}
|
||||
@@ -170,7 +188,9 @@ var vm = new Vue({
|
||||
$("#qrcode-modal").modal("show");
|
||||
},
|
||||
genDownloadURL: function (f) {
|
||||
return location.origin + "/" + f.path;
|
||||
var search = location.search;
|
||||
var sep = search == "" ? "?" : "&"
|
||||
return location.origin + this.getEncodePath(f.name) + location.search + sep + "download=true";
|
||||
},
|
||||
shouldHaveQrcode: function (name) {
|
||||
return ['apk', 'ipa'].indexOf(getExtention(name)) !== -1;
|
||||
@@ -216,11 +236,12 @@ 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") {
|
||||
return true;
|
||||
window.location.href = reqPath;
|
||||
return;
|
||||
}
|
||||
var reqPath = pathJoin([location.pathname, f.name]);
|
||||
loadFileOrDir(reqPath);
|
||||
e.preventDefault()
|
||||
},
|
||||
@@ -231,34 +252,41 @@ var vm = new Vue({
|
||||
showInfo: function (f) {
|
||||
console.log(f);
|
||||
$.ajax({
|
||||
url: pathJoin(["/-/info", location.pathname, f.name]),
|
||||
url: this.getEncodePath(f.name),
|
||||
data: {
|
||||
op: "info",
|
||||
},
|
||||
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("Directory name?")
|
||||
var name = window.prompt("current path: " + location.pathname + "\nplease enter the new directory name", "")
|
||||
console.log(name)
|
||||
if (!name) {
|
||||
return
|
||||
}
|
||||
if(!checkPathNameLegal(name)) {
|
||||
alert("Name should not contains any of \\/:*<>|")
|
||||
return
|
||||
}
|
||||
$.ajax({
|
||||
url: pathJoin(["/-/mkdir", location.pathname]),
|
||||
data: {
|
||||
name: name,
|
||||
},
|
||||
url: this.getEncodePath(name),
|
||||
method: "POST",
|
||||
success: function (res) {
|
||||
console.log(res)
|
||||
loadFileList()
|
||||
},
|
||||
error: function (err) {
|
||||
alert(err.responseText);
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
showErrorMessage(jqXHR)
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -270,18 +298,19 @@ var vm = new Vue({
|
||||
}
|
||||
}
|
||||
$.ajax({
|
||||
url: pathJoin([location.pathname, f.name]),
|
||||
url: this.getEncodePath(f.name),
|
||||
method: 'DELETE',
|
||||
success: function (res) {
|
||||
loadFileList()
|
||||
},
|
||||
error: function (err) {
|
||||
alert(err.responseText);
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
showErrorMessage(jqXHR)
|
||||
}
|
||||
});
|
||||
},
|
||||
updateBreadcrumb: function () {
|
||||
var pathname = decodeURI(location.pathname || "/");
|
||||
updateBreadcrumb: function (pathname) {
|
||||
var pathname = decodeURI(pathname || location.pathname || "/");
|
||||
pathname = pathname.split('?')[0]
|
||||
var parts = pathname.split('/');
|
||||
this.breadcrumb = [];
|
||||
if (pathname == "/") {
|
||||
@@ -307,23 +336,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.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.contentHTML = '<pre>' + res + '</pre>';
|
||||
console.log("Finally")
|
||||
})
|
||||
.done(function (res) {
|
||||
console.log("done", res)
|
||||
});
|
||||
},
|
||||
loadAll: function () {
|
||||
// TODO: move loadFileList here
|
||||
@@ -340,16 +369,23 @@ window.onpopstate = function (event) {
|
||||
}
|
||||
|
||||
function loadFileOrDir(reqPath) {
|
||||
window.history.pushState({}, "", reqPath);
|
||||
loadFileList(reqPath)
|
||||
let requestUri = reqPath + location.search
|
||||
var retObj = loadFileList(requestUri)
|
||||
if (retObj !== null) {
|
||||
retObj.done(function () {
|
||||
window.history.pushState({}, "", requestUri);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function loadFileList(pathname) {
|
||||
var pathname = pathname || location.pathname;
|
||||
// console.log("load filelist:", pathname)
|
||||
var pathname = pathname || location.pathname + location.search;
|
||||
var retObj = null
|
||||
if (getQueryString("raw") !== "false") { // not a file preview
|
||||
$.ajax({
|
||||
url: pathJoin(["/-/json", pathname]),
|
||||
var sep = pathname.indexOf("?") === -1 ? "?" : "&"
|
||||
retObj = $.ajax({
|
||||
url: pathname + sep + "json=true",
|
||||
dataType: "json",
|
||||
cache: false,
|
||||
success: function (res) {
|
||||
@@ -357,22 +393,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 (err) {
|
||||
console.error(err)
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
showErrorMessage(jqXHR)
|
||||
},
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
vm.updateBreadcrumb();
|
||||
vm.previewMode = getQueryString("raw") == "false";
|
||||
if (vm.previewMode) {
|
||||
vm.loadPreviewFile();
|
||||
}
|
||||
return retObj
|
||||
}
|
||||
|
||||
Vue.filter('fromNow', function (value) {
|
||||
@@ -407,10 +443,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();
|
||||
});
|
||||
|
||||
31
assets/themes/cyan.css
Normal file
31
assets/themes/cyan.css
Normal file
@@ -0,0 +1,31 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// +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")
|
||||
@@ -1,23 +0,0 @@
|
||||
// +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)
|
||||
}
|
||||
}
|
||||
3
build.sh
3
build.sh
@@ -23,13 +23,10 @@ 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
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
FROM golang:1.10
|
||||
WORKDIR /go/src/github.com/codeskyblue/gohttpserver
|
||||
ADD . /go/src/github.com/codeskyblue/gohttpserver/
|
||||
RUN go get -v
|
||||
FROM golang:1.16
|
||||
WORKDIR /app/gohttpserver
|
||||
ADD . /app/gohttpserver
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags '-X main.VERSION=docker' -o gohttpserver
|
||||
|
||||
FROM debian:stretch
|
||||
@@ -10,7 +9,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 /go/src/github.com/codeskyblue/gohttpserver/gohttpserver .
|
||||
COPY --from=0 /app/gohttpserver/gohttpserver .
|
||||
EXPOSE 8000
|
||||
ENTRYPOINT [ "/app/gohttpserver", "--root=/app/public" ]
|
||||
CMD []
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
FROM golang:1.10
|
||||
WORKDIR /go/src/github.com/codeskyblue/gohttpserver
|
||||
ADD . /go/src/github.com/codeskyblue/gohttpserver/
|
||||
RUN go get -v
|
||||
FROM golang:1.16
|
||||
WORKDIR /appsrc/gohttpserver
|
||||
ADD . /appsrc/gohttpserver
|
||||
RUN GOOS=linux GOARCH=arm go build -ldflags '-X main.VERSION=docker' -o gohttpserver .
|
||||
|
||||
FROM multiarch/debian-debootstrap:armhf-stretch
|
||||
@@ -10,7 +9,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 /go/src/github.com/codeskyblue/gohttpserver/gohttpserver .
|
||||
COPY --from=0 /appsrc/gohttpserver/gohttpserver .
|
||||
EXPOSE 8000
|
||||
ENTRYPOINT [ "/app/gohttpserver", "--root=/app/public" ]
|
||||
CMD []
|
||||
|
||||
@@ -9,8 +9,12 @@ fi
|
||||
|
||||
set -ex
|
||||
|
||||
sed -i '/experimental/d' $HOME/.docker/config.json
|
||||
sed -i '1a"experimental": "enabled",' $HOME/.docker/config.json
|
||||
if test $(uname) = "Linux"
|
||||
then
|
||||
sed -i '/experimental/d' $HOME/.docker/config.json
|
||||
sed -i '1a"experimental": "enabled",' $HOME/.docker/config.json
|
||||
fi
|
||||
|
||||
docker manifest create codeskyblue/gohttpserver \
|
||||
codeskyblue/gohttpserver:latest \
|
||||
codeskyblue/gohttpserver:armhf
|
||||
@@ -21,4 +25,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
16
go.mod
@@ -1,9 +1,11 @@
|
||||
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
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect
|
||||
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
|
||||
@@ -13,9 +15,11 @@ 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
|
||||
github.com/pkg/errors v0.8.0 // indirect
|
||||
github.com/shogo82148/androidbinary v0.0.0-20180627093851-01c4bfa8b3b5
|
||||
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
|
||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||
github.com/stretchr/testify v1.3.0
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect
|
||||
golang.org/x/text v0.3.3
|
||||
howett.net/plist v0.0.0-20201203080718-1454fab16a06 // indirect
|
||||
)
|
||||
|
||||
74
go.sum
Normal file
74
go.sum
Normal file
@@ -0,0 +1,74 @@
|
||||
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=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
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=
|
||||
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=
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"regexp"
|
||||
@@ -41,8 +42,14 @@ 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
|
||||
@@ -53,13 +60,15 @@ type HTTPStaticServer struct {
|
||||
|
||||
indexes []IndexFileItem
|
||||
m *mux.Router
|
||||
bufPool sync.Pool // use sync.Pool caching buf to reduce gc ratio
|
||||
}
|
||||
|
||||
func NewHTTPStaticServer(root string) *HTTPStaticServer {
|
||||
if root == "" {
|
||||
root = "./"
|
||||
}
|
||||
root = filepath.ToSlash(root)
|
||||
// if root == "" {
|
||||
// root = "./"
|
||||
// }
|
||||
// root = filepath.ToSlash(root)
|
||||
root = filepath.ToSlash(filepath.Clean(root))
|
||||
if !strings.HasSuffix(root, "/") {
|
||||
root = root + "/"
|
||||
}
|
||||
@@ -69,6 +78,9 @@ func NewHTTPStaticServer(root string) *HTTPStaticServer {
|
||||
Root: root,
|
||||
Theme: "black",
|
||||
m: m,
|
||||
bufPool: sync.Pool{
|
||||
New: func() interface{} { return make([]byte, 32*1024) },
|
||||
},
|
||||
}
|
||||
|
||||
go func() {
|
||||
@@ -83,20 +95,12 @@ func NewHTTPStaticServer(root string) *HTTPStaticServer {
|
||||
}
|
||||
}()
|
||||
|
||||
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)
|
||||
|
||||
// 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.hUpload).Methods("POST")
|
||||
m.HandleFunc("/{path:.*}", s.hUploadOrMkdir).Methods("POST")
|
||||
m.HandleFunc("/{path:.*}", s.hDelete).Methods("DELETE")
|
||||
return s
|
||||
}
|
||||
@@ -105,18 +109,48 @@ 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"]
|
||||
relPath := filepath.Join(s.Root, path)
|
||||
log.Println("GET", path, relPath)
|
||||
if r.FormValue("raw") == "false" || isDir(relPath) {
|
||||
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) {
|
||||
if r.Method == "HEAD" {
|
||||
return
|
||||
}
|
||||
renderHTML(w, "index.html", s)
|
||||
renderHTML(w, "assets/index.html", s)
|
||||
} else {
|
||||
if filepath.Base(path) == YAMLCONF {
|
||||
auth := s.readAccessConf(path)
|
||||
auth := s.readAccessConf(realPath)
|
||||
if !auth.Delete {
|
||||
http.Error(w, "Security warning, not allowed to read", http.StatusForbidden)
|
||||
return
|
||||
@@ -125,65 +159,63 @@ 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)))
|
||||
}
|
||||
http.ServeFile(w, r, relPath)
|
||||
http.ServeFile(w, r, realPath)
|
||||
}
|
||||
}
|
||||
|
||||
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"]
|
||||
auth := s.readAccessConf(path)
|
||||
if !auth.canDelete(req) {
|
||||
http.Error(w, "Mkdir forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
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 {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
w.Write([]byte("Success"))
|
||||
}
|
||||
|
||||
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)
|
||||
realPath := s.getRealPath(req)
|
||||
// path = filepath.Clean(path) // for safe reason, prevent path contain ..
|
||||
auth := s.readAccessConf(realPath)
|
||||
if !auth.canDelete(req) {
|
||||
http.Error(w, "Delete forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
err := os.Remove(filepath.Join(s.Root, path))
|
||||
|
||||
// TODO: path safe check
|
||||
err := os.RemoveAll(realPath)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
pathErr, ok := err.(*os.PathError)
|
||||
if ok {
|
||||
http.Error(w, pathErr.Op+" "+path+": "+pathErr.Err.Error(), 500)
|
||||
} else {
|
||||
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)
|
||||
func (s *HTTPStaticServer) hUploadOrMkdir(w http.ResponseWriter, req *http.Request) {
|
||||
dirpath := s.getRealPath(req)
|
||||
|
||||
// check auth
|
||||
auth := s.readAccessConf(path)
|
||||
auth := s.readAccessConf(dirpath)
|
||||
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)
|
||||
@@ -204,19 +236,54 @@ func (s *HTTPStaticServer) hUpload(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
defer dst.Close()
|
||||
if _, err := io.Copy(dst, file); err != nil {
|
||||
|
||||
// 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 {
|
||||
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,
|
||||
@@ -253,11 +320,8 @@ func parseApkInfo(path string) (ai *ApkInfo) {
|
||||
|
||||
func (s *HTTPStaticServer) hInfo(w http.ResponseWriter, r *http.Request) {
|
||||
path := mux.Vars(r)["path"]
|
||||
relPath := filepath.Join(s.Root, path)
|
||||
if !isFile(relPath) {
|
||||
http.Error(w, "Not a file", 403)
|
||||
return
|
||||
}
|
||||
relPath := s.getRealPath(r)
|
||||
|
||||
fi, err := os.Stat(relPath)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
@@ -276,6 +340,8 @@ 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"
|
||||
}
|
||||
@@ -285,8 +351,7 @@ func (s *HTTPStaticServer) hInfo(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) hZip(w http.ResponseWriter, r *http.Request) {
|
||||
path := mux.Vars(r)["path"]
|
||||
CompressToZip(w, filepath.Join(s.Root, path))
|
||||
CompressToZip(w, s.getRealPath(r))
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) hUnzip(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -303,13 +368,9 @@ func (s *HTTPStaticServer) hUnzip(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func genURLStr(r *http.Request, path string) *url.URL {
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
func combineURL(r *http.Request, path string) *url.URL {
|
||||
return &url.URL{
|
||||
Scheme: scheme,
|
||||
Scheme: r.URL.Scheme,
|
||||
Host: r.Host,
|
||||
Path: path,
|
||||
}
|
||||
@@ -322,7 +383,7 @@ func (s *HTTPStaticServer) hPlist(w http.ResponseWriter, r *http.Request) {
|
||||
path = path[0:len(path)-6] + ".ipa"
|
||||
}
|
||||
|
||||
relPath := filepath.Join(s.Root, path)
|
||||
relPath := s.getRealPath(r)
|
||||
plinfo, err := parseIPA(relPath)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
@@ -348,9 +409,11 @@ 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"]
|
||||
plistUrl := genURLStr(r, "/-/ipa/plist/"+path).String()
|
||||
if r.TLS == nil {
|
||||
// send plist to plistproxy and get a https link
|
||||
var plistUrl string
|
||||
|
||||
if r.URL.Scheme == "https" {
|
||||
plistUrl = combineURL(r, "/-/ipa/plist/"+path).String()
|
||||
} else if s.PlistProxy != "" {
|
||||
httpPlistLink := "http://" + r.Host + "/-/ipa/plist/" + path
|
||||
url, err := s.genPlistLink(httpPlistLink)
|
||||
if err != nil {
|
||||
@@ -358,11 +421,14 @@ 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, "ipa-install.html", map[string]string{
|
||||
renderHTML(w, "assets/ipa-install.html", map[string]string{
|
||||
"Name": filepath.Base(path),
|
||||
"PlistLink": plistUrl,
|
||||
})
|
||||
@@ -397,8 +463,7 @@ func (s *HTTPStaticServer) genPlistLink(httpPlistLink string) (plistUrl string,
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) hFileOrDirectory(w http.ResponseWriter, r *http.Request) {
|
||||
path := mux.Vars(r)["path"]
|
||||
http.ServeFile(w, r, filepath.Join(s.Root, path))
|
||||
http.ServeFile(w, r, s.getRealPath(r))
|
||||
}
|
||||
|
||||
type HTTPFileInfo struct {
|
||||
@@ -501,9 +566,9 @@ func (c *AccessConf) canUpload(r *http.Request) bool {
|
||||
|
||||
func (s *HTTPStaticServer) hJSONList(w http.ResponseWriter, r *http.Request) {
|
||||
requestPath := mux.Vars(r)["path"]
|
||||
localPath := filepath.Join(s.Root, requestPath)
|
||||
realPath := s.getRealPath(r)
|
||||
search := r.FormValue("search")
|
||||
auth := s.readAccessConf(requestPath)
|
||||
auth := s.readAccessConf(realPath)
|
||||
auth.Upload = auth.canUpload(r)
|
||||
auth.Delete = auth.canDelete(r)
|
||||
|
||||
@@ -521,7 +586,7 @@ func (s *HTTPStaticServer) hJSONList(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
infos, err := ioutil.ReadDir(localPath)
|
||||
infos, err := ioutil.ReadDir(realPath)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
@@ -550,7 +615,7 @@ func (s *HTTPStaticServer) hJSONList(w http.ResponseWriter, r *http.Request) {
|
||||
lr.Name = filepath.ToSlash(name) // fix for windows
|
||||
}
|
||||
if info.IsDir() {
|
||||
name := deepPath(localPath, info.Name())
|
||||
name := deepPath(realPath, info.Name())
|
||||
lr.Name = name
|
||||
lr.Path = filepath.Join(filepath.Dir(path), name)
|
||||
lr.Type = "dir"
|
||||
@@ -570,7 +635,7 @@ func (s *HTTPStaticServer) hJSONList(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
var dirSizeMap = make(map[string]int64)
|
||||
var dirInfoSize = Directory{size: make(map[string]int64), mutex: &sync.RWMutex{}}
|
||||
|
||||
func (s *HTTPStaticServer) makeIndex() error {
|
||||
var indexes = make([]IndexFileItem, 0)
|
||||
@@ -590,21 +655,28 @@ func (s *HTTPStaticServer) makeIndex() error {
|
||||
return nil
|
||||
})
|
||||
s.indexes = indexes
|
||||
dirSizeMap = make(map[string]int64)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) historyDirSize(dir string) int64 {
|
||||
var size int64
|
||||
if size, ok := dirSizeMap[dir]; ok {
|
||||
dirInfoSize.mutex.RLock()
|
||||
size, ok := dirInfoSize.size[dir]
|
||||
dirInfoSize.mutex.RUnlock()
|
||||
|
||||
if ok {
|
||||
return size
|
||||
}
|
||||
|
||||
for _, fitem := range s.indexes {
|
||||
if filepath.HasPrefix(fitem.Path, dir) {
|
||||
size += fitem.Info.Size()
|
||||
}
|
||||
}
|
||||
dirSizeMap[dir] = size
|
||||
|
||||
dirInfoSize.mutex.Lock()
|
||||
dirInfoSize.size[dir] = size
|
||||
dirInfoSize.mutex.Unlock()
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
@@ -641,19 +713,19 @@ func (s *HTTPStaticServer) defaultAccessConf() AccessConf {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) readAccessConf(requestPath string) (ac AccessConf) {
|
||||
requestPath = filepath.Clean(requestPath)
|
||||
if requestPath == "/" || requestPath == "" || requestPath == "." {
|
||||
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
|
||||
ac = s.defaultAccessConf()
|
||||
realPath = s.Root
|
||||
} else {
|
||||
parentPath := filepath.Dir(requestPath)
|
||||
parentPath := filepath.Dir(realPath)
|
||||
ac = s.readAccessConf(parentPath)
|
||||
}
|
||||
relPath := filepath.Join(s.Root, requestPath)
|
||||
if isFile(relPath) {
|
||||
relPath = filepath.Dir(relPath)
|
||||
if isFile(realPath) {
|
||||
realPath = filepath.Dir(realPath)
|
||||
}
|
||||
cfgFile := filepath.Join(relPath, YAMLCONF)
|
||||
cfgFile := filepath.Join(realPath, YAMLCONF)
|
||||
data, err := ioutil.ReadFile(cfgFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
@@ -669,10 +741,9 @@ func (s *HTTPStaticServer) readAccessConf(requestPath string) (ac AccessConf) {
|
||||
}
|
||||
|
||||
func deepPath(basedir, name string) string {
|
||||
isDir := true
|
||||
// loop max 5, incase of for loop not finished
|
||||
maxDepth := 5
|
||||
for depth := 0; depth <= maxDepth && isDir; depth += 1 {
|
||||
for depth := 0; depth <= maxDepth; depth += 1 {
|
||||
finfos, err := ioutil.ReadDir(filepath.Join(basedir, name))
|
||||
if err != nil || len(finfos) != 1 {
|
||||
break
|
||||
@@ -686,16 +757,6 @@ func deepPath(basedir, name string) 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 {
|
||||
@@ -734,7 +795,7 @@ var (
|
||||
_tmpls = make(map[string]*template.Template)
|
||||
)
|
||||
|
||||
func executeTemplate(w http.ResponseWriter, name string, v interface{}) {
|
||||
func renderHTML(w http.ResponseWriter, name string, v interface{}) {
|
||||
if t, ok := _tmpls[name]; ok {
|
||||
t.Execute(w, v)
|
||||
return
|
||||
@@ -744,16 +805,6 @@ func executeTemplate(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 \\/:*<>|")
|
||||
|
||||
56
main.go
56
main.go
@@ -10,6 +10,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -20,7 +21,7 @@ import (
|
||||
"github.com/go-yaml/yaml"
|
||||
"github.com/goji/httpauth"
|
||||
"github.com/gorilla/handlers"
|
||||
_ "github.com/shurcooL/vfsgen"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type Configure struct {
|
||||
@@ -28,6 +29,7 @@ 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"`
|
||||
@@ -102,6 +104,7 @@ func parseFlags() error {
|
||||
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)
|
||||
@@ -134,6 +137,17 @@ 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 main() {
|
||||
if err := parseFlags(); err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -144,7 +158,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)
|
||||
ss.Prefix = gcfg.Prefix
|
||||
ss.Theme = gcfg.Theme
|
||||
ss.Title = gcfg.Title
|
||||
ss.GoogleTrackerID = gcfg.GoogleTrackerID
|
||||
@@ -160,6 +181,9 @@ 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
|
||||
|
||||
@@ -177,6 +201,8 @@ func main() {
|
||||
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
|
||||
@@ -187,15 +213,26 @@ func main() {
|
||||
hdlr = handlers.ProxyHeaders(hdlr)
|
||||
}
|
||||
|
||||
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")
|
||||
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) {
|
||||
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)
|
||||
@@ -206,11 +243,16 @@ 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 = http.ListenAndServeTLS(gcfg.Addr, gcfg.Cert, gcfg.Key, nil)
|
||||
err = srv.ListenAndServeTLS(gcfg.Cert, gcfg.Key)
|
||||
} else {
|
||||
err = http.ListenAndServe(gcfg.Addr, nil)
|
||||
err = srv.ListenAndServe()
|
||||
}
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
27
oauth2-proxy.go
Normal file
27
oauth2-proxy.go
Normal file
@@ -0,0 +1,27 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
25
utils.go
25
utils.go
@@ -3,6 +3,8 @@ package main
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -71,3 +73,26 @@ 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()
|
||||
}
|
||||
|
||||
@@ -25,3 +25,25 @@ 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
56
zip.go
@@ -14,6 +14,7 @@ import (
|
||||
"strings"
|
||||
|
||||
dkignore "github.com/codeskyblue/dockerignore"
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
)
|
||||
|
||||
type Zip struct {
|
||||
@@ -130,3 +131,58 @@ 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
|
||||
}
|
||||
|
||||
11
zip_test.go
11
zip_test.go
@@ -3,13 +3,18 @@ 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)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Nil(t, err)
|
||||
t.Log("Content: " + buf.String())
|
||||
}
|
||||
|
||||
//func TestUnzipTo(t *testing.T){
|
||||
// err := unzipFile("testdata.zip", "./tmp")
|
||||
// assert.Nil(t, err)
|
||||
//}
|
||||
|
||||
Reference in New Issue
Block a user