Compare commits
154 commits
Author | SHA1 | Date | |
---|---|---|---|
Augusto Dwenger J. | 627dea49c5 | ||
777ff2376d | |||
41981a3d8a | |||
47789eec68 | |||
4c880193ce | |||
86aeecfac2 | |||
Augusto Dwenger J. | 284d332424 | ||
Augusto Dwenger J. | 2e58b052f9 | ||
e8af97872c | |||
d7590729f1 | |||
f7063651c2 | |||
42d1e077c9 | |||
1258ac759f | |||
a60a9c4811 | |||
b6200a88c0 | |||
1618440f4f | |||
f676d25787 | |||
e24ff056f7 | |||
833443dfaf | |||
b6f8563d18 | |||
20ed231c53 | |||
b4470cf4bd | |||
83c202fde3 | |||
65f98b227a | |||
74545dd8bc | |||
400b503587 | |||
3846857b38 | |||
aa3eabc9e2 | |||
Augusto Dwenger J. | 6eb81b7579 | ||
Augusto Dwenger J. | ecc6de50a1 | ||
Augusto Dwenger J. | e58ae4bd33 | ||
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 | |||
21ef8634cf | |||
392f251bc2 | |||
727a1cdbfa | |||
5333393394 | |||
6556afc08a | |||
c012da6e2e | |||
44fb4d2d09 | |||
64299ce9f9 | |||
4f6226d83d | |||
afe8cb7692 | |||
a80617c85f | |||
fac40bac1c | |||
cdc5b318a0 | |||
b0403b60d9 | |||
d7b886751b | |||
22555ed424 | |||
1c60ccc233 | |||
3175093adf | |||
72a9609acb | |||
b2923eaa4a | |||
c3218a122f | |||
65bb5c6302 | |||
0cacad260e | |||
40f02fba65 | |||
e5bb87aaa1 | |||
80b84ad6b9 | |||
395537a5d2 | |||
dc03b85283 | |||
56aec4c702 | |||
951128c735 | |||
8e4187fe19 | |||
403a2ea84b | |||
7f4444b8a3 | |||
bd7666b3e6 | |||
876b8cffc4 | |||
6bfa914a51 | |||
24ef16e80e | |||
cc6b6987f8 | |||
1b217ca440 | |||
7587b8067a | |||
2b7fb84a5e | |||
963f2ddb32 | |||
3ff2620ba5 | |||
22c83d8f2d | |||
cab407a098 | |||
4d21cd5dd8 | |||
26cb73de1c | |||
caef8c0fbd | |||
ea3d3fdcef | |||
4d2c5587d6 | |||
6b3f0f0ae6 | |||
28e9a2c0ca | |||
e5677bec86 | |||
a1b16115df | |||
5439149b09 | |||
332276f6db | |||
66c45e1c6e | |||
68654f8bd3 | |||
a95e38802d | |||
e063187656 | |||
892546343a | |||
dc3b5695d4 | |||
f8ee0c0459 | |||
947bbe9efa | |||
9af2b5b5f6 | |||
7e1387a787 | |||
ff1f38c494 | |||
de1d2ba243 | |||
2ab2c553b6 | |||
6889309af7 | |||
61758ec3ce | |||
7a1831c6ec | |||
31cafdfd16 | |||
859574f1e0 | |||
b4efbd1b6b | |||
a7fc7054c5 | |||
9dd2d6da30 | |||
ad85705599 | |||
b94703f6b9 | |||
acad061682 | |||
d89a841bfb | |||
b6a2908b68 | |||
37b71cb7bf | |||
644cbbcfd6 | |||
f9b2518734 |
47
.drone.yml
Normal file
|
@ -0,0 +1,47 @@
|
|||
kind: pipeline
|
||||
type: docker
|
||||
name: tests
|
||||
|
||||
steps:
|
||||
- name: unit-test
|
||||
image: golang
|
||||
volumes:
|
||||
- name: cache
|
||||
path: /go
|
||||
commands:
|
||||
- go test -coverprofile=coverage.out -covermode=count ./...
|
||||
- go tool cover -func=coverage.out | grep total
|
||||
|
||||
- name: race-test
|
||||
image: golang
|
||||
volumes:
|
||||
- name: cache
|
||||
path: /go
|
||||
commands:
|
||||
- go test -race ./...
|
||||
|
||||
volumes:
|
||||
- name: cache
|
||||
temp: {}
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: docker-build
|
||||
|
||||
steps:
|
||||
- name: docker
|
||||
image: plugins/docker
|
||||
settings:
|
||||
registry: registry.hhhammer.de
|
||||
username: ci
|
||||
password:
|
||||
from_secret: DOCKER_REGISTRY_KEY
|
||||
repo: registry.hhhammer.de/gohttpserver
|
||||
dockerfile: docker/Dockerfile
|
||||
auto_tag: true
|
||||
pull_image: true
|
||||
|
||||
depends_on:
|
||||
- tests
|
||||
|
5
.gitignore
vendored
|
@ -23,8 +23,11 @@ _testmain.go
|
|||
*.test
|
||||
*.prof
|
||||
|
||||
dist/
|
||||
|
||||
gohttpserver
|
||||
bindata_assetfs.go
|
||||
assets_vfsdata.go
|
||||
*.un~
|
||||
*.swp
|
||||
|
||||
dist/
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
language: go
|
||||
go:
|
||||
- 1.5
|
||||
- 1.6
|
||||
env:
|
||||
global:
|
||||
- GO15VENDOREXPERIMENT=1
|
||||
script:
|
||||
- go test -v
|
59
Godeps/Godeps.json
generated
|
@ -1,59 +0,0 @@
|
|||
{
|
||||
"ImportPath": "github.com/codeskyblue/gohttpserver",
|
||||
"GoVersion": "go1.6",
|
||||
"GodepVersion": "v74",
|
||||
"Deps": [
|
||||
{
|
||||
"ImportPath": "github.com/DHowett/go-plist",
|
||||
"Rev": "f4bf55d2395500aacc17eedcebc5f139336b4312"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/alecthomas/kingpin",
|
||||
"Comment": "v2.1.3",
|
||||
"Rev": "aef28d186e59d39ed537473dfce4472108ea1045"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/alecthomas/template",
|
||||
"Rev": "b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/alecthomas/template/parse",
|
||||
"Rev": "b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/alecthomas/units",
|
||||
"Rev": "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/codeskyblue/dockerignore",
|
||||
"Rev": "de82dee623d9207f906d327172149cba50427a88"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/go-yaml/yaml",
|
||||
"Rev": "e4d366fc3c7938e2958e662b4258c7a89e1f0e3e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/goji/httpauth",
|
||||
"Rev": "2da839ab0f4df05a6db5eb277995589dadbd4fb9"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/gorilla/context",
|
||||
"Comment": "v1.1-4-gaed02d1",
|
||||
"Rev": "aed02d124ae4a0e94fea4541c8effd05bf0c8296"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/gorilla/handlers",
|
||||
"Comment": "v1.1-10-g801d6e3",
|
||||
"Rev": "801d6e3b008914ee888c9ab9b1b379b9a56fbf44"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/gorilla/mux",
|
||||
"Comment": "v1.1-15-gd391bea",
|
||||
"Rev": "d391bea3118c9fc17a88d62c9189bb791255e0ef"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/mash/go-accesslog",
|
||||
"Rev": "9ba8e13f36087d6cb83d9a9f17f9e8da137d5ee9"
|
||||
}
|
||||
]
|
||||
}
|
5
Godeps/Readme
generated
|
@ -1,5 +0,0 @@
|
|||
This directory tree is generated automatically by godep.
|
||||
|
||||
Please do not edit.
|
||||
|
||||
See https://github.com/tools/godep for more information.
|
3
LICENSE
|
@ -1,6 +1,7 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 shengxiang
|
||||
Copyright (c) 2020 Augusto Dwenger J.
|
||||
Copyright (c) 2018 shengxiang
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
219
README.md
|
@ -1,19 +1,17 @@
|
|||
# gohttpserver
|
||||
[![Build Status](https://travis-ci.org/codeskyblue/gohttpserver.svg?branch=master)](https://travis-ci.org/codeskyblue/gohttpserver)
|
||||
This is a fork from [codeskyblue/gohttpserver](https://github.com/codeskyblue/gohttpserver/) without google-analytics.
|
||||
|
||||
Make the best HTTP File Server. Better UI, upload support, apple&android install package qrcode generate.
|
||||
- Goal: Make the best HTTP File Server.
|
||||
- Features: Human-friendly UI, file uploading support, direct QR-code generation for Apple & Android install package.
|
||||
|
||||
[Demo site](https://gohttpserver.herokuapp.com/)
|
||||
|
||||
- 目标: 做最好的HTTP文件服务器
|
||||
- 功能: 人性化的UI体验,文件的上传支持,安卓和苹果安装包的二维码直接生成。
|
||||
|
||||
**Binary** can be download from [github releases](https://github.com/codeskyblue/gohttpserver/releases/)
|
||||
|
||||
## Notes
|
||||
If using go1.5, ensure you set GO15VENDOREXPERIMENT=1
|
||||
|
||||
Upload size now limited to 1G
|
||||
## Requirements
|
||||
Tested with go-1.16
|
||||
|
||||
## Screenshots
|
||||
![screen](testdata/filetypes/gohttpserver.gif)
|
||||
|
@ -24,7 +22,7 @@ Upload size now limited to 1G
|
|||
1. [x] All assets package to Standalone binary
|
||||
1. [x] Different file type different icon
|
||||
1. [x] Support show or hide hidden files
|
||||
1. [x] Upload support (for security reason, you need enabled it by option `--upload`)
|
||||
1. [x] Upload support (auth by token or session)
|
||||
1. [x] README.md preview
|
||||
1. [x] HTTP Basic Auth
|
||||
1. [x] Partial reload pages when directory change
|
||||
|
@ -47,9 +45,15 @@ Upload size now limited to 1G
|
|||
1. [ ] Support sort by size or modified time
|
||||
1. [x] Add version info into index page
|
||||
1. [ ] Add api `/-/info/some.(apk|ipa)` to get detail info
|
||||
1. [x] Add api `/-/apk/info/some.apk` to get android package info
|
||||
1. [x] Auto tag version
|
||||
1. [x] Custom title support
|
||||
1. [x] Support setting from conf file
|
||||
1. [x] Quick copy download link
|
||||
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
|
||||
```
|
||||
|
@ -57,23 +61,107 @@ go get -v github.com/codeskyblue/gohttpserver
|
|||
cd $GOPATH/src/github.com/codeskyblue/gohttpserver
|
||||
go build && ./gohttpserver
|
||||
```
|
||||
|
||||
## Usage
|
||||
Listen port 8000 on all interface, and enable upload
|
||||
Listen on port 8000 of all interfaces, and enable file uploading.
|
||||
|
||||
```
|
||||
./gohttpserver -r ./ --addr :8000 --upload
|
||||
./gohttpserver -r ./ --port 8000 --upload
|
||||
```
|
||||
|
||||
Use command `gohttpserver --help` to see more usage.
|
||||
|
||||
## Docker Usage
|
||||
share current directory
|
||||
|
||||
```bash
|
||||
docker run -it --rm -p 8000:8000 -v $PWD:/app/public --name gohttpserver registry.hhhammer.de/gohttpserver
|
||||
```
|
||||
|
||||
Share current directory with http basic auth
|
||||
|
||||
```bash
|
||||
docker run -it --rm -p 8000:8000 -v $PWD:/app/public --name gohttpserver \
|
||||
registry.hhhammer.de/gohttpserver \
|
||||
--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 \
|
||||
registry.hhhammer.de/gohttpserver \
|
||||
--auth-type openid
|
||||
```
|
||||
|
||||
To build image yourself, please change the PWD to the root of this repo.
|
||||
|
||||
```bash
|
||||
cd gohttpserver/
|
||||
docker build -t registry.hhhhammer.de/gohttpserver -f docker/Dockerfile .
|
||||
```
|
||||
|
||||
## Authentication options
|
||||
- Enable basic http authentication
|
||||
|
||||
```sh
|
||||
$ gohttpserver --auth-type http --auth-http username:password
|
||||
```
|
||||
|
||||
- Use openid auth
|
||||
|
||||
```sh
|
||||
$ 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
|
||||
$ gohttpserver --upload
|
||||
```
|
||||
|
||||
- Enable delete and Create folder
|
||||
|
||||
```sh
|
||||
$ gohttpserver --delete
|
||||
```
|
||||
|
||||
## Advanced usage
|
||||
Support update access rule if there is a file named `.ghs.yml` under directory. `.ghs.yml` example
|
||||
Add access rule by creating a `.ghs.yml` file under a sub-directory. An example:
|
||||
|
||||
```yaml
|
||||
---
|
||||
upload: false
|
||||
delete: false
|
||||
users:
|
||||
- email: "codeskyblue@codeskyblue.com"
|
||||
delete: true
|
||||
upload: true
|
||||
token: 4567gf8asydhf293r23r
|
||||
```
|
||||
|
||||
For example, if there is such file under directory `foo`, directory `foo` can not be uploaded, while `bar` can.
|
||||
In this case, if openid auth is enabled and user "codeskyblue@codeskyblue.com" has logged in, he/she can delete/upload files under the directory where the `.ghs.yml` file exits.
|
||||
|
||||
`token` is used for upload. see [upload with curl](#upload-with-curl)
|
||||
|
||||
For example, in the following directory hierarchy, users can delete/uploade files in directory `foo`, but he/she cannot do this in directory `bar`.
|
||||
|
||||
```
|
||||
root -
|
||||
|
@ -84,62 +172,114 @@ root -
|
|||
`-- hello.txt
|
||||
```
|
||||
|
||||
Use config file. specfied with `--conf`, see [example config.yml](testdata/config.yml). Note that command line option can overwrite conf in `config.yml`
|
||||
User can specify config file name with `--conf`, see [example config.yml](testdata/config.yml).
|
||||
|
||||
To specify which files is hidden and which file is visible, add the following lines to `.ghs.yml`
|
||||
|
||||
```yaml
|
||||
accessTables:
|
||||
- regex: block.file
|
||||
allow: false
|
||||
- regex: visual.file
|
||||
allow: true
|
||||
```
|
||||
|
||||
### ipa plist proxy
|
||||
This is used for server which not https enabled. default use <https://plistproxy.herokuapp.com/plist>
|
||||
This is used for server on which https is enabled. default use <https://plistproxy.herokuapp.com/plist>
|
||||
|
||||
```
|
||||
./gohttpserver --plistproxy=https://someproxyhost.com/
|
||||
```
|
||||
|
||||
Proxy web site should have ability
|
||||
Test if proxy works:
|
||||
|
||||
```sh
|
||||
$ http POST https://proxyhost.com/plist < app.plist
|
||||
$ http POST https://someproxyhost.com/plist < app.plist
|
||||
{
|
||||
"key": "18f99211"
|
||||
}
|
||||
$ http GET https://proxyhost.com/plist/18f99211
|
||||
$ 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`
|
||||
|
||||
PS: max upload size limited to 1G (hard coded)
|
||||
|
||||
```sh
|
||||
$ curl -F file=@foo.txt localhost:8000/somedir
|
||||
{"destination":"somedir/foo.txt","success":true}
|
||||
# upload with token
|
||||
$ curl -F file=@foo.txt -F token=12312jlkjafs localhost:8000/somedir
|
||||
{"destination":"somedir/foo.txt","success":true}
|
||||
|
||||
# upload and change filename
|
||||
$ 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
|
||||
Recommended configuration, assume your gohttpserver listening on `127.0.0.1:8200`
|
||||
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain-name.com;
|
||||
|
||||
location / {
|
||||
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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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>
|
||||
|
||||
## FAQ
|
||||
- [How to generate self signed certificate with openssl](http://stackoverflow.com/questions/10175812/how-to-create-a-self-signed-certificate-with-openssl)
|
||||
|
||||
### How the search works
|
||||
The search algorithm follow the search engine google. keywords are seperated with space, words with prefix `-` will be excluded.
|
||||
### How the query is formated
|
||||
The search query follows common format rules just like Google. Keywords are seperated with space(s), keywords with prefix `-` will be excluded in search results.
|
||||
|
||||
1. `hello world` means must contains `hello` and `world`
|
||||
1. `hello -world` means must contains `hello` but not contains `world`
|
||||
|
||||
## Developer Guide
|
||||
Depdencies are managed by godep
|
||||
Depdencies are managed by [govendor](https://github.com/kardianos/govendor)
|
||||
|
||||
```sh
|
||||
go get -v github.com/tools/godep
|
||||
go get github.com/jteeuwen/go-bindata/...
|
||||
go get github.com/elazarl/go-bindata-assetfs/...
|
||||
```
|
||||
1. Build develop version. **assets** directory must exists
|
||||
|
||||
Theme are all defined in [res/themes](res/themes) directory. Now only two, black and green.
|
||||
```sh
|
||||
go build
|
||||
./gohttpserver
|
||||
```
|
||||
2. Build single binary release
|
||||
|
||||
## How to build single binary release
|
||||
```sh
|
||||
go-bindata-assetfs -tags bindata res/...
|
||||
go build -tags bindata
|
||||
```
|
||||
```sh
|
||||
go generate .
|
||||
go build -tags vfs
|
||||
```
|
||||
|
||||
Theme are defined in [assets/themes](assets/themes) directory. Now only two themes are available, "black" and "green".
|
||||
|
||||
That's all. ^_^
|
||||
|
||||
## Reference Web sites
|
||||
|
||||
|
@ -150,14 +290,17 @@ That's all. ^_^
|
|||
* Markdown CSS <https://github.com/sindresorhus/github-markdown-css>
|
||||
* Upload support <http://www.dropzonejs.com/>
|
||||
* ScrollUp <https://markgoodyear.com/2013/01/scrollup-jquery-plugin/>
|
||||
* Clipboard <https://clipboardjs.com/>
|
||||
* Underscore <http://underscorejs.org/>
|
||||
|
||||
**Go Libraries**
|
||||
|
||||
* <https://github.com/elazarl/go-bindata-assetfs>
|
||||
* [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>
|
||||
|
||||
## History
|
||||
The first version is <https://github.com/codeskyblue/gohttp>
|
||||
The old version is hosted at <https://github.com/codeskyblue/gohttp>
|
||||
|
||||
## LICENSE
|
||||
This project is under license [MIT](LICENSE)
|
||||
This project is licensed under [MIT](LICENSE).
|
||||
|
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
@ -24,3 +24,11 @@ div.dropzone {
|
|||
.qrcode-title {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.clearfix::after {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
#qrcodeCanvas {
|
||||
padding-right: 20px;
|
||||
}
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 382 KiB After Width: | Height: | Size: 382 KiB |
Before Width: | Height: | Size: 698 B After Width: | Height: | Size: 698 B |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
301
assets/index.html
Normal file
|
@ -0,0 +1,301 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<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="/-/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">
|
||||
<nav class="navbar navbar-default">
|
||||
<div class="container">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-2">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="/">[[.Title]]</a>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-2">
|
||||
<ul class="nav navbar-nav">
|
||||
<li class="hidden-xs">
|
||||
<a href="javascript:void(0)" v-on:click='genQrcode()'>
|
||||
View in Phone
|
||||
<span class="glyphicon glyphicon-qrcode"></span>
|
||||
</a>
|
||||
</li>
|
||||
[[if eq .AuthType "openid"]]
|
||||
<template v-if="!user.email">
|
||||
<a href="/-/login" class="btn btn-sm btn-default navbar-btn">
|
||||
Sign in <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]]
|
||||
[[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="submit">
|
||||
<span class="glyphicon glyphicon-search"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
<ul id="nav-right-bar" class="nav navbar-nav navbar-right">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container">
|
||||
<div class="col-md-12">
|
||||
<ol class="breadcrumb">
|
||||
<li>
|
||||
<a v-on:click='changePath("/", $event)' href="/"><i class="fa fa-home"></i></a>
|
||||
</li>
|
||||
<li v-for="bc in breadcrumb.slice(0, breadcrumb.length-1)">
|
||||
<a v-on:click='changePath(bc.path, $event)' href="{{bc.path}}">{{bc.name}}</a>
|
||||
</li>
|
||||
<li v-if="breadcrumb.length >= 1">
|
||||
{{breadcrumb.slice(-1)[0].name}}
|
||||
</li>
|
||||
</ol>
|
||||
<table class="table table-hover" v-if="!previewMode">
|
||||
<thead>
|
||||
<tr>
|
||||
<td colspan=4>
|
||||
<!-- <button class="btn btn-xs btn-default" v-on:click='toggleHidden()'>
|
||||
Back <i class="fa" v-bind:class='showHidden ? "fa-eye" : "fa-eye-slash"'></i>
|
||||
</button> -->
|
||||
<div>
|
||||
<button class="btn btn-xs btn-default" onclick="history.back()">
|
||||
Back <i class="fa fa-arrow-left"></i>
|
||||
</button>
|
||||
<button class="btn btn-xs btn-default" v-on:click='toggleHidden()'>
|
||||
Hidden <i class="fa" v-bind:class='showHidden ? "fa-eye" : "fa-eye-slash"'></i>
|
||||
</button>
|
||||
<button class="btn btn-xs btn-default" v-show="auth.upload" data-toggle="modal" data-target="#upload-modal">
|
||||
Upload <i class="fa fa-upload"></i>
|
||||
</button>
|
||||
<button class="btn btn-xs btn-default" v-show="auth.delete" @click="makeDirectory">
|
||||
New Folder <i class="fa fa-folder"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Size</th>
|
||||
<th class="hidden-xs">
|
||||
<span style="cursor: pointer" v-on:click='mtimeTypeFromNow = !mtimeTypeFromNow'>ModTime</span>
|
||||
</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="f in computedFiles">
|
||||
<td>
|
||||
<a v-on:click='clickFileOrDir(f, $event)' href="{{getEncodePath(f)}}">
|
||||
<!-- ?raw=false -->
|
||||
<i style="padding-right: 0.5em" class="fa" v-bind:class='genFileClass(f)'></i> {{f.name}}
|
||||
</a>
|
||||
<!-- for search -->
|
||||
<button v-show="f.type == 'file' && f.name.indexOf('/') >= 0" class="btn btn-default btn-xs" @click="changeParentDirectory(f.path)">
|
||||
<i class="fa fa-folder-open-o"></i>
|
||||
</button>
|
||||
</td>
|
||||
<td><span v-if="f.type == 'dir'">~</span> {{f.size | formatBytes}}</td>
|
||||
<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="{{getEncodePath(f)}}/?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="{{genDownloadURL(f)}}">
|
||||
<span class="hidden-xs">Download</span>
|
||||
<span class="glyphicon glyphicon-download-alt"></span>
|
||||
</a>
|
||||
<button class="btn btn-default btn-xs bstooltip" data-trigger="manual" data-title="Copied!"
|
||||
data-clipboard-text="{{genDownloadURL(f)}}">
|
||||
<i class="fa fa-copy"></i>
|
||||
</button>
|
||||
<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 hidden-xs" v-on:click="genQrcode(f.name)">
|
||||
<span v-if="shouldHaveQrcode(f.name)">QRCode</span>
|
||||
<span class="glyphicon glyphicon-qrcode"></span>
|
||||
</button>
|
||||
<a class="btn btn-default btn-xs visible-xs" v-if="shouldHaveQrcode(f.name)" href="{{genInstallURL(f.name)}}">
|
||||
Install <i class="fa fa-cube"></i>
|
||||
</a>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-12" id="preview" v-if="preview.filename">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title" style="font-weight: normal">
|
||||
<i class="fa" v-bind:class='genFileClass(previewFile)'></i> {{preview.filename}}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<article class="markdown-body">{{{preview.contentHTML }}}
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12" id="content">
|
||||
<!-- Small qrcode modal -->
|
||||
<div id="qrcode-modal" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title">
|
||||
<span id="qrcode-title"></span>
|
||||
<a style="font-size: 0.6em" href="#" id="qrcode-link">[view]</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body clearfix">
|
||||
<div id="qrcodeCanvas" class="pull-left"></div>
|
||||
<div id="qrcodeRight" class="pull-left">
|
||||
<p>
|
||||
<a href="#">下载链接</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Upload modal-->
|
||||
<div id="upload-modal" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title">
|
||||
<i class="fa fa-upload"></i> File upload
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form action="#" class="dropzone" id="upload-form"></form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" @click="removeAllUploads">RemoveAll</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- File info modal -->
|
||||
<div id="file-info-modal" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title">
|
||||
<span id="file-info-title"></span>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre id="file-info-content"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<div id="footer" class="pull-right" style="margin: 2em 1em">
|
||||
<a href="https://github.com/hamburghammer/gohttpserver">gohttpserver (ver:{{version}})</a>, fork from <a href="https://github.com/codeskyblue/gohttpserver">codeskyblue/gohttpserver</a>.
|
||||
Lizensed under <a href="https://github.com/hamburghammer/gohttpserver/blob/master/LICENSE">MIT</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> -->
|
||||
<!--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) {
|
||||
i['GoogleAnalyticsObject'] = r;
|
||||
i[r] = i[r] || function () {
|
||||
(i[r].q = i[r].q || []).push(arguments)
|
||||
}, i[r].l = 1 * new Date();
|
||||
a = s.createElement(o),
|
||||
m = s.getElementsByTagName(o)[0];
|
||||
a.async = 1;
|
||||
a.src = g;
|
||||
m.parentNode.insertBefore(a, m)
|
||||
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
|
||||
|
||||
ga('create', '[[.GoogleTrackerID]]', 'auto');
|
||||
ga('send', 'pageview');
|
||||
</script> [[ end ]]
|
||||
</body>
|
||||
|
||||
</html>
|
71
assets/ipa-install.html
Normal file
|
@ -0,0 +1,71 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<title>[[.Name]] install</title>
|
||||
<meta http-equiv="Content-Type" content="text/HTML; charset=utf-8">
|
||||
<meta content="target-densitydpi=device-dpi,width=640" name="viewport" id="viewport">
|
||||
<link rel="shortcut icon" type="image/png" href="/-/assets/favicon.png" />
|
||||
<script type="text/javascript" src="/-/assets/js/ua-parser.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
function showById(name) {
|
||||
document.getElementById(name).style.display = 'block';
|
||||
}
|
||||
|
||||
function checkBrowerAndDownload() {
|
||||
var parser = new UAParser();
|
||||
var os_info = parser.getOS();
|
||||
console.log(os_info)
|
||||
|
||||
var plistLink = "[[.PlistLink]]";
|
||||
var ipaInstallLink = 'itms-services://?action=download-manifest&url=' + plistLink;
|
||||
document.getElementById('itms-link').href = ipaInstallLink;
|
||||
|
||||
// wechat is support AppStore link now.
|
||||
if (navigator.userAgent.toLowerCase().match(/MicroMessenger/i) == "micromessenger") {
|
||||
showById('safari');
|
||||
location.href = ipaInstallLink;
|
||||
return;
|
||||
} else if (os_info.name == 'Android') {
|
||||
showById("android");
|
||||
return;
|
||||
} else if (os_info.name == 'iOS') {
|
||||
showById('safari');
|
||||
location.href = ipaInstallLink;
|
||||
return;
|
||||
} else {
|
||||
showById('browser');
|
||||
return;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<style>
|
||||
#wechat {
|
||||
position: relative;
|
||||
width: 640px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
min-height: 777px;
|
||||
}
|
||||
</style>
|
||||
<div id="wechat" style="display: none">
|
||||
<img style='width: 100%;position: relative;' src='/-/assets/imgs/wx.png' />
|
||||
</div>
|
||||
<div id="browser" style="display: none">
|
||||
This is IPA install page, you should open this link with your iPhone.
|
||||
</div>
|
||||
<div id="safari" style="display: none">
|
||||
If install not started soon, click <a id="itms-link" href="#">here</a>
|
||||
</div>
|
||||
<div id="android" style="display: none">
|
||||
This is IPA install page, not for android.
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
checkBrowerAndDownload();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
7
assets/js/clipboard-1.5.12.min.js
vendored
Normal file
454
assets/js/index.js
Normal file
|
@ -0,0 +1,454 @@
|
|||
jQuery('#qrcodeCanvas').qrcode({
|
||||
text: "http://jetienne.com/"
|
||||
});
|
||||
|
||||
Dropzone.autoDiscover = false;
|
||||
|
||||
function getExtention(fname) {
|
||||
return fname.slice((fname.lastIndexOf(".") - 1 >>> 0) + 2);
|
||||
}
|
||||
|
||||
function pathJoin(parts, sep) {
|
||||
var separator = sep || '/';
|
||||
var replace = new RegExp(separator + '{1,}', 'g');
|
||||
return parts.join(separator).replace(replace, separator);
|
||||
}
|
||||
|
||||
function getQueryString(name) {
|
||||
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
|
||||
var r = decodeURI(window.location.search).substr(1).match(reg);
|
||||
if (r != null) return r[2].replace(/\+/g, ' ');
|
||||
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: {
|
||||
user: {
|
||||
email: "",
|
||||
name: "",
|
||||
},
|
||||
location: window.location,
|
||||
breadcrumb: [],
|
||||
showHidden: false,
|
||||
previewMode: false,
|
||||
preview: {
|
||||
filename: '',
|
||||
filetype: '',
|
||||
filesize: 0,
|
||||
contentHTML: '',
|
||||
},
|
||||
version: "loading",
|
||||
mtimeTypeFromNow: false, // or fromNow
|
||||
auth: {},
|
||||
search: getQueryString("search"),
|
||||
files: [{
|
||||
name: "loading ...",
|
||||
path: "",
|
||||
size: "...",
|
||||
type: "dir",
|
||||
}],
|
||||
myDropzone: null,
|
||||
},
|
||||
computed: {
|
||||
computedFiles: function () {
|
||||
var that = this;
|
||||
that.preview.filename = null;
|
||||
|
||||
var files = this.files.filter(function (f) {
|
||||
if (f.name == 'README.md') {
|
||||
that.preview.filename = f.name;
|
||||
}
|
||||
if (!that.showHidden && f.name.slice(0, 1) === '.') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// console.log(this.previewFile)
|
||||
if (this.preview.filename) {
|
||||
var name = this.preview.filename; // For now only README.md
|
||||
console.log(pathJoin([location.pathname, 'README.md']))
|
||||
$.ajax({
|
||||
url: pathJoin([location.pathname, 'README.md']),
|
||||
method: 'GET',
|
||||
success: function (res) {
|
||||
var converter = new showdown.Converter({
|
||||
tables: true,
|
||||
omitExtraWLInCodeBlocks: true,
|
||||
parseImgDimensions: true,
|
||||
simplifiedAutoLink: true,
|
||||
literalMidWordUnderscores: true,
|
||||
tasklists: true,
|
||||
ghCodeBlocks: true,
|
||||
smoothLivePreview: true,
|
||||
simplifiedAutoLink: true,
|
||||
strikethrough: true,
|
||||
});
|
||||
|
||||
var html = converter.makeHtml(res);
|
||||
that.preview.contentHTML = html;
|
||||
},
|
||||
error: function (err) {
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return files;
|
||||
},
|
||||
},
|
||||
created: function () {
|
||||
$.ajax({
|
||||
url: "/-/user",
|
||||
method: "get",
|
||||
dataType: "json",
|
||||
success: function (ret) {
|
||||
if (ret) {
|
||||
this.user.email = ret.email;
|
||||
this.user.name = ret.name;
|
||||
}
|
||||
}.bind(this)
|
||||
})
|
||||
this.myDropzone = new Dropzone("#upload-form", {
|
||||
paramName: "file",
|
||||
maxFilesize: 10240,
|
||||
addRemoveLinks: true,
|
||||
init: function () {
|
||||
this.on("uploadprogress", function (file, progress) {
|
||||
// console.log("File progress", progress);
|
||||
});
|
||||
this.on("complete", function (file) {
|
||||
console.log("reload file list")
|
||||
loadFileList()
|
||||
})
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
getEncodePath: function (f) {
|
||||
return pathJoin([location.pathname, encodeURIComponent(f.name)]);
|
||||
},
|
||||
formatTime: function (timestamp) {
|
||||
var m = moment(timestamp);
|
||||
if (this.mtimeTypeFromNow) {
|
||||
return m.fromNow();
|
||||
}
|
||||
return m.format('YYYY-MM-DD HH:mm:ss');
|
||||
},
|
||||
toggleHidden: function () {
|
||||
this.showHidden = !this.showHidden;
|
||||
},
|
||||
removeAllUploads: function () {
|
||||
this.myDropzone.removeAllFiles();
|
||||
},
|
||||
parentDirectory: function (path) {
|
||||
return path.replace('\\', '/').split('/').slice(0, -1).join('/')
|
||||
},
|
||||
changeParentDirectory: function (path) {
|
||||
var parentDir = this.parentDirectory(path);
|
||||
loadFileOrDir(parentDir);
|
||||
},
|
||||
genInstallURL: function (name, noEncode) {
|
||||
var parts = [location.host];
|
||||
var pathname = decodeURI(location.pathname);
|
||||
if (!name) {
|
||||
parts.push(pathname);
|
||||
} else if (getExtention(name) == "ipa") {
|
||||
parts.push("/-/ipa/link", pathname, encodeURIComponent(name));
|
||||
} else {
|
||||
parts.push(pathname, name);
|
||||
}
|
||||
var urlPath = location.protocol + "//" + pathJoin(parts);
|
||||
return noEncode ? urlPath : encodeURI(urlPath);
|
||||
},
|
||||
genQrcode: function (name, title) {
|
||||
var urlPath = this.genInstallURL(name, true);
|
||||
$("#qrcode-title").html(title || name || location.pathname);
|
||||
$("#qrcode-link").attr("href", urlPath);
|
||||
$('#qrcodeCanvas').empty().qrcode({
|
||||
text: encodeURI(urlPath),
|
||||
});
|
||||
|
||||
$("#qrcodeRight a").attr("href", urlPath);
|
||||
$("#qrcode-modal").modal("show");
|
||||
},
|
||||
genDownloadURL: function (f) {
|
||||
var search = location.search;
|
||||
var sep = search == "" ? "?" : "&"
|
||||
return location.origin + this.getEncodePath(f) + location.search + sep + "download=true";
|
||||
},
|
||||
shouldHaveQrcode: function (name) {
|
||||
return ['apk', 'ipa'].indexOf(getExtention(name)) !== -1;
|
||||
},
|
||||
genFileClass: function (f) {
|
||||
if (f.type == "dir") {
|
||||
if (f.name == '.git') {
|
||||
return 'fa-git-square';
|
||||
}
|
||||
return "fa-folder-open";
|
||||
}
|
||||
var ext = getExtention(f.name);
|
||||
switch (ext) {
|
||||
case "go":
|
||||
case "py":
|
||||
case "js":
|
||||
case "java":
|
||||
case "c":
|
||||
case "cpp":
|
||||
case "h":
|
||||
return "fa-file-code-o";
|
||||
case "pdf":
|
||||
return "fa-file-pdf-o";
|
||||
case "zip":
|
||||
return "fa-file-zip-o";
|
||||
case "mp3":
|
||||
case "wav":
|
||||
return "fa-file-audio-o";
|
||||
case "jpg":
|
||||
case "png":
|
||||
case "gif":
|
||||
case "jpeg":
|
||||
case "tiff":
|
||||
return "fa-file-picture-o";
|
||||
case "ipa":
|
||||
case "dmg":
|
||||
return "fa-apple";
|
||||
case "apk":
|
||||
return "fa-android";
|
||||
case "exe":
|
||||
return "fa-windows";
|
||||
}
|
||||
return "fa-file-text-o"
|
||||
},
|
||||
clickFileOrDir: function (f, e) {
|
||||
var reqPath = pathJoin([location.pathname, encodeURIComponent(f.name)]);
|
||||
// TODO: fix here tomorrow
|
||||
if (f.type == "file") {
|
||||
window.location.href = reqPath;
|
||||
return;
|
||||
}
|
||||
loadFileOrDir(reqPath);
|
||||
e.preventDefault()
|
||||
},
|
||||
changePath: function (reqPath, e) {
|
||||
loadFileOrDir(reqPath);
|
||||
e.preventDefault()
|
||||
},
|
||||
showInfo: function (f) {
|
||||
console.log(f);
|
||||
$.ajax({
|
||||
url: pathJoin(["/", location.pathname, encodeURIComponent(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("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(["/", location.pathname, "/", encodeURIComponent(name)]),
|
||||
method: "POST",
|
||||
success: function (res) {
|
||||
console.log(res)
|
||||
loadFileList()
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
showErrorMessage(jqXHR)
|
||||
}
|
||||
})
|
||||
},
|
||||
deletePathConfirm: function (f, e) {
|
||||
e.preventDefault();
|
||||
if (!e.altKey) { // skip confirm when alt pressed
|
||||
if (!window.confirm("Delete " + location.pathname + "/" + f.name + " ?")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
$.ajax({
|
||||
url: pathJoin([location.pathname, encodeURIComponent(f.name)]),
|
||||
method: 'DELETE',
|
||||
success: function (res) {
|
||||
loadFileList()
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
showErrorMessage(jqXHR)
|
||||
}
|
||||
});
|
||||
},
|
||||
updateBreadcrumb: function (pathname) {
|
||||
var pathname = decodeURI(pathname || location.pathname || "/");
|
||||
pathname = pathname.split('?')[0]
|
||||
var parts = pathname.split('/');
|
||||
this.breadcrumb = [];
|
||||
if (pathname == "/") {
|
||||
return this.breadcrumb;
|
||||
}
|
||||
var i = 2;
|
||||
for (; i <= parts.length; i += 1) {
|
||||
var name = parts[i - 1];
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
var path = parts.slice(0, i).join('/');
|
||||
this.breadcrumb.push({
|
||||
name: name + (i == parts.length ? ' /' : ''),
|
||||
path: path
|
||||
})
|
||||
}
|
||||
return this.breadcrumb;
|
||||
},
|
||||
loadPreviewFile: function (filepath, e) {
|
||||
if (e) {
|
||||
e.preventDefault() // may be need a switch
|
||||
}
|
||||
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)
|
||||
});
|
||||
},
|
||||
loadAll: function () {
|
||||
// TODO: move loadFileList here
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
window.onpopstate = function (event) {
|
||||
if (location.search.match(/\?search=/)) {
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
loadFileList()
|
||||
}
|
||||
|
||||
function loadFileOrDir(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 + location.search;
|
||||
var retObj = null
|
||||
if (getQueryString("raw") !== "false") { // not a file preview
|
||||
var sep = pathname.indexOf("?") === -1 ? "?" : "&"
|
||||
retObj = $.ajax({
|
||||
url: pathname + sep + "json=true",
|
||||
dataType: "json",
|
||||
cache: false,
|
||||
success: function (res) {
|
||||
res.files = _.sortBy(res.files, function (f) {
|
||||
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)
|
||||
},
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
vm.previewMode = getQueryString("raw") == "false";
|
||||
if (vm.previewMode) {
|
||||
vm.loadPreviewFile();
|
||||
}
|
||||
return retObj
|
||||
}
|
||||
|
||||
Vue.filter('fromNow', function (value) {
|
||||
return moment(value).fromNow();
|
||||
})
|
||||
|
||||
Vue.filter('formatBytes', function (value) {
|
||||
var bytes = parseFloat(value);
|
||||
if (bytes < 0) return "-";
|
||||
else if (bytes < 1024) return bytes + " B";
|
||||
else if (bytes < 1048576) return (bytes / 1024).toFixed(0) + " KB";
|
||||
else if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + " MB";
|
||||
else return (bytes / 1073741824).toFixed(1) + " GB";
|
||||
})
|
||||
|
||||
$(function () {
|
||||
$.scrollUp({
|
||||
scrollText: '', // text are defined in css
|
||||
});
|
||||
|
||||
// For page first loading
|
||||
loadFileList(location.pathname + location.search)
|
||||
|
||||
// update version
|
||||
$.getJSON("/-/sysinfo", function (res) {
|
||||
vm.version = res.version;
|
||||
})
|
||||
|
||||
var clipboard = new Clipboard('.btn');
|
||||
clipboard.on('success', function (e) {
|
||||
console.info('Action:', e.action);
|
||||
console.info('Text:', e.text);
|
||||
console.info('Trigger:', e.trigger);
|
||||
$(e.trigger)
|
||||
.tooltip('show')
|
||||
.mouseleave(function () {
|
||||
$(this).tooltip('hide');
|
||||
})
|
||||
|
||||
e.clearSelection();
|
||||
});
|
||||
});
|
5
assets/js/showdown-1.6.4.min.js
vendored
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;
|
||||
}
|
9
assets_dev.go
Normal 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
|
@ -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)
|
||||
}
|
||||
}
|
4
build.sh
|
@ -23,12 +23,12 @@ fi
|
|||
build() {
|
||||
echo "$1 $2 ..."
|
||||
GOOS=$1 GOARCH=$2 go build \
|
||||
-tags bindata \
|
||||
-tags vfs \
|
||||
-ldflags "$LDFLAGS" \
|
||||
-o dist/gohttpserver-${3:-""}
|
||||
}
|
||||
|
||||
go-bindata-assetfs -tags bindata res/...
|
||||
go generate .
|
||||
|
||||
build linux arm linux-arm
|
||||
build darwin amd64 mac-amd64
|
||||
|
|
14
docker/Dockerfile
Normal file
|
@ -0,0 +1,14 @@
|
|||
FROM docker.io/golang:1.16 AS build
|
||||
WORKDIR /app/gohttpserver
|
||||
ADD . /app/gohttpserver
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags '-X main.VERSION=docker' -o gohttpserver
|
||||
|
||||
FROM docker.io/alpine:latest
|
||||
RUN mkdir -p /app/public
|
||||
VOLUME /app/public
|
||||
WORKDIR /app
|
||||
ADD assets ./assets
|
||||
COPY --from=build /app/gohttpserver/gohttpserver .
|
||||
EXPOSE 8000
|
||||
ENTRYPOINT [ "/app/gohttpserver", "--root=/app/public" ]
|
||||
CMD []
|
15
docker/Dockerfile.armhf
Normal file
|
@ -0,0 +1,15 @@
|
|||
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
|
||||
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 /appsrc/gohttpserver/gohttpserver .
|
||||
EXPOSE 8000
|
||||
ENTRYPOINT [ "/app/gohttpserver", "--root=/app/public" ]
|
||||
CMD []
|
18
docker/push_images
Normal file
|
@ -0,0 +1,18 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# article: https://lantian.pub/article/modify-computer/build-arm-docker-image-on-x86-docker-hub-travis-automatic-build.lantian
|
||||
|
||||
set -ex
|
||||
|
||||
docker run --rm --privileged multiarch/qemu-user-static:register --reset
|
||||
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
||||
|
||||
IMAGE_NAME="gohttpserver"
|
||||
|
||||
# arm linux for respberry
|
||||
docker build -t $DOCKER_USERNAME/$IMAGE_NAME:armhf -f docker/Dockerfile.armhf .
|
||||
|
||||
# x86 linux
|
||||
docker build -t $DOCKER_USERNAME/$IMAGE_NAME:latest -f docker/Dockerfile .
|
||||
|
||||
docker push $DOCKER_USERNAME/$IMAGE_NAME
|
28
docker/push_manifest
Normal file
|
@ -0,0 +1,28 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
|
||||
# push manifest
|
||||
if [[ ! -d $HOME/.docker ]]
|
||||
then
|
||||
mkdir $HOME/.docker
|
||||
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
|
||||
|
||||
docker manifest create codeskyblue/gohttpserver \
|
||||
codeskyblue/gohttpserver:latest \
|
||||
codeskyblue/gohttpserver:armhf
|
||||
docker manifest annotate codeskyblue/gohttpserver \
|
||||
codeskyblue/gohttpserver:latest --os linux --arch amd64
|
||||
docker manifest annotate codeskyblue/gohttpserver \
|
||||
codeskyblue/gohttpserver:armhf --os linux --arch arm --variant v7
|
||||
docker manifest push codeskyblue/gohttpserver
|
||||
|
||||
# check again
|
||||
docker run mplatform/mquery codeskyblue/gohttpserver
|
27
go.mod
Normal file
|
@ -0,0 +1,27 @@
|
|||
module github.com/hamburghammer/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/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
|
||||
github.com/fork2fix/go-plist v0.0.0-20181126021357-36960be5e636
|
||||
github.com/go-yaml/yaml v2.1.0+incompatible
|
||||
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d
|
||||
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/shogo82148/androidbinary v0.0.0-20180627093851-01c4bfa8b3b5
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371 // indirect
|
||||
github.com/shurcooL/vfsgen v0.0.0-20181020040650-a97a25d856ca
|
||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||
github.com/stretchr/testify v1.3.0
|
||||
golang.org/x/text v0.3.3
|
||||
golang.org/x/tools v0.1.0 // indirect
|
||||
howett.net/plist v0.0.0-20201203080718-1454fab16a06 // indirect
|
||||
)
|
91
go.sum
Normal file
|
@ -0,0 +1,91 @@
|
|||
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/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371 h1:SWV2fHctRpRrp49VXJ6UZja7gU9QLHwRpIPBN89SKEo=
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/vfsgen v0.0.0-20181020040650-a97a25d856ca h1:3fECS8atRjByijiI8yYiuwLwQ2ZxXobW7ua/8GRB3pI=
|
||||
github.com/shurcooL/vfsgen v0.0.0-20181020040650-a97a25d856ca/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
|
||||
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.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
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/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/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=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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=
|
|
@ -3,6 +3,9 @@ package main
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
|
@ -13,28 +16,44 @@ import (
|
|||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"regexp"
|
||||
|
||||
"github.com/go-yaml/yaml"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/shogo82148/androidbinary/apk"
|
||||
)
|
||||
|
||||
const YAMLCONF = ".ghs.yml"
|
||||
|
||||
type ApkInfo struct {
|
||||
PackageName string `json:"packageName"`
|
||||
MainActivity string `json:"mainActivity"`
|
||||
Version struct {
|
||||
Code int `json:"code"`
|
||||
Name string `json:"name"`
|
||||
} `json:"version"`
|
||||
}
|
||||
|
||||
type IndexFileItem struct {
|
||||
Path string
|
||||
Info os.FileInfo
|
||||
}
|
||||
|
||||
type HTTPStaticServer struct {
|
||||
Root string
|
||||
Upload bool
|
||||
Delete bool
|
||||
Title string
|
||||
Theme string
|
||||
PlistProxy string
|
||||
GoogleTrackerId string
|
||||
Root string
|
||||
Upload bool
|
||||
Delete bool
|
||||
Title string
|
||||
Theme string
|
||||
PlistProxy string
|
||||
AuthType string
|
||||
|
||||
indexes []IndexFileItem
|
||||
m *mux.Router
|
||||
bufPool sync.Pool // use sync.Pool caching buf to reduce gc ratio
|
||||
}
|
||||
|
||||
func NewHTTPStaticServer(root string) *HTTPStaticServer {
|
||||
|
@ -51,6 +70,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() {
|
||||
|
@ -65,17 +87,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("/{path:.*}", s.hIndex).Methods("GET")
|
||||
m.HandleFunc("/{path:.*}", s.hUpload).Methods("POST")
|
||||
m.HandleFunc("/{path:.*}", s.hIndex).Methods("GET", "HEAD")
|
||||
m.HandleFunc("/{path:.*}", s.hUploadOrMkdir).Methods("POST")
|
||||
m.HandleFunc("/{path:.*}", s.hDelete).Methods("DELETE")
|
||||
return s
|
||||
}
|
||||
|
@ -87,11 +104,35 @@ func (s *HTTPStaticServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
func (s *HTTPStaticServer) hIndex(w http.ResponseWriter, r *http.Request) {
|
||||
path := mux.Vars(r)["path"]
|
||||
relPath := filepath.Join(s.Root, path)
|
||||
if r.FormValue("json") == "true" {
|
||||
s.hJSONList(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
finfo, err := os.Stat(relPath)
|
||||
if err == nil && finfo.IsDir() {
|
||||
tmpl.ExecuteTemplate(w, "index", s)
|
||||
if r.FormValue("op") == "info" {
|
||||
s.hInfo(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if r.FormValue("op") == "archive" {
|
||||
s.hZip(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("GET", path, relPath)
|
||||
if r.FormValue("raw") == "false" || isDir(relPath) {
|
||||
if r.Method == "HEAD" {
|
||||
return
|
||||
}
|
||||
renderHTML(w, "index.html", s)
|
||||
} else {
|
||||
if filepath.Base(path) == YAMLCONF {
|
||||
auth := s.readAccessConf(path)
|
||||
if !auth.Delete {
|
||||
http.Error(w, "Security warning, not allowed to read", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
if r.FormValue("download") == "true" {
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+strconv.Quote(filepath.Base(path)))
|
||||
}
|
||||
|
@ -99,22 +140,20 @@ func (s *HTTPStaticServer) hIndex(w http.ResponseWriter, r *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) hDelete(w http.ResponseWriter, req *http.Request) {
|
||||
// only can delete file now
|
||||
path := mux.Vars(req)["path"]
|
||||
func (s *HTTPStaticServer) hMkdir(w http.ResponseWriter, req *http.Request) {
|
||||
path := filepath.Dir(mux.Vars(req)["path"])
|
||||
auth := s.readAccessConf(path)
|
||||
log.Printf("%#v", auth)
|
||||
if !auth.Delete {
|
||||
http.Error(w, "Delete forbidden", http.StatusForbidden)
|
||||
if !auth.canDelete(req) {
|
||||
http.Error(w, "Mkdir forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
err := os.Remove(filepath.Join(s.Root, path))
|
||||
|
||||
name := filepath.Base(mux.Vars(req)["path"])
|
||||
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
|
||||
|
@ -122,48 +161,191 @@ func (s *HTTPStaticServer) hDelete(w http.ResponseWriter, req *http.Request) {
|
|||
w.Write([]byte("Success"))
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) hUpload(w http.ResponseWriter, req *http.Request) {
|
||||
func (s *HTTPStaticServer) hDelete(w http.ResponseWriter, req *http.Request) {
|
||||
path := mux.Vars(req)["path"]
|
||||
path = filepath.Clean(path) // for safe reason, prevent path contain ..
|
||||
auth := s.readAccessConf(path)
|
||||
if !auth.canDelete(req) {
|
||||
http.Error(w, "Delete forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: path safe check
|
||||
err := os.RemoveAll(filepath.Join(s.Root, path))
|
||||
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)
|
||||
}
|
||||
return
|
||||
}
|
||||
w.Write([]byte("Success"))
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) hUploadOrMkdir(w http.ResponseWriter, req *http.Request) {
|
||||
path := mux.Vars(req)["path"]
|
||||
dirpath := filepath.Join(s.Root, path)
|
||||
|
||||
// check auth
|
||||
auth := s.readAccessConf(path)
|
||||
if !auth.Upload {
|
||||
if !auth.canUpload(req) {
|
||||
http.Error(w, "Upload forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
err := req.ParseMultipartForm(1 << 30) // max memory 1G
|
||||
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)
|
||||
return
|
||||
}
|
||||
if len(req.MultipartForm.File["file"]) == 0 {
|
||||
http.Error(w, "Need multipart file", http.StatusInternalServerError)
|
||||
defer func() {
|
||||
file.Close()
|
||||
req.MultipartForm.RemoveAll() // Seen from go source code, req.MultipartForm not nil after call FormFile(..)
|
||||
}()
|
||||
|
||||
filename := req.FormValue("filename")
|
||||
if filename == "" {
|
||||
filename = header.Filename
|
||||
}
|
||||
if err := checkFilename(filename); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
dirpath := filepath.Join(s.Root, path)
|
||||
dstPath := filepath.Join(dirpath, filename)
|
||||
|
||||
for _, mfile := range req.MultipartForm.File["file"] {
|
||||
file, err := mfile.Open()
|
||||
defer file.Close()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
dst, err := os.Create(filepath.Join(dirpath, mfile.Filename)) // BUG(ssx): There is a leak here
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
// 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
|
||||
}
|
||||
w.Write([]byte("Upload success"))
|
||||
|
||||
// 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,
|
||||
})
|
||||
}
|
||||
|
||||
type FileJSONInfo struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Size int64 `json:"size"`
|
||||
Path string `json:"path"`
|
||||
ModTime int64 `json:"mtime"`
|
||||
Extra interface{} `json:"extra,omitempty"`
|
||||
}
|
||||
|
||||
// path should be absolute
|
||||
func parseApkInfo(path string) (ai *ApkInfo) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Println("parse-apk-info panic:", err)
|
||||
}
|
||||
}()
|
||||
apkf, err := apk.OpenFile(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ai = &ApkInfo{}
|
||||
ai.MainActivity, _ = apkf.MainActivity()
|
||||
ai.PackageName = apkf.PackageName()
|
||||
ai.Version.Code = apkf.Manifest().VersionCode
|
||||
ai.Version.Name = apkf.Manifest().VersionName
|
||||
return
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) hInfo(w http.ResponseWriter, r *http.Request) {
|
||||
path := mux.Vars(r)["path"]
|
||||
relPath := filepath.Join(s.Root, path)
|
||||
|
||||
fi, err := os.Stat(relPath)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
fji := &FileJSONInfo{
|
||||
Name: fi.Name(),
|
||||
Size: fi.Size(),
|
||||
Path: path,
|
||||
ModTime: fi.ModTime().UnixNano() / 1e6,
|
||||
}
|
||||
ext := filepath.Ext(path)
|
||||
switch ext {
|
||||
case ".md":
|
||||
fji.Type = "markdown"
|
||||
case ".apk":
|
||||
fji.Type = "apk"
|
||||
fji.Extra = parseApkInfo(relPath)
|
||||
case "":
|
||||
fji.Type = "dir"
|
||||
default:
|
||||
fji.Type = "text"
|
||||
}
|
||||
data, _ := json.Marshal(fji)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) hZip(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -185,13 +367,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,
|
||||
}
|
||||
|
@ -230,9 +408,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 {
|
||||
|
@ -240,17 +420,17 @@ func (s *HTTPStaticServer) hIpaLink(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
plistUrl = url
|
||||
//plistUrl = strings.TrimSuffix(s.PlistProxy, "/") + "/" + r.Host + "/-/ipa/plist/" + path
|
||||
} else {
|
||||
http.Error(w, "500: Server should be https:// or provide valid plistproxy", 500)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
tmpl.ExecuteTemplate(w, "ipa-install", map[string]string{
|
||||
log.Println("PlistURL:", plistUrl)
|
||||
renderHTML(w, "ipa-install.html", map[string]string{
|
||||
"Name": filepath.Base(path),
|
||||
"PlistLink": plistUrl,
|
||||
})
|
||||
// w.Write([]byte(fmt.Sprintf(
|
||||
// `<a href='itms-services://?action=download-manifest&url=%s'>Click this link to install</a>`,
|
||||
// plistUrl)))
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) genPlistLink(httpPlistLink string) (plistUrl string, err error) {
|
||||
|
@ -286,23 +466,111 @@ func (s *HTTPStaticServer) hFileOrDirectory(w http.ResponseWriter, r *http.Reque
|
|||
http.ServeFile(w, r, filepath.Join(s.Root, path))
|
||||
}
|
||||
|
||||
type ListResponse struct {
|
||||
type HTTPFileInfo struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Type string `json:"type"`
|
||||
Size string `json:"size"`
|
||||
Size int64 `json:"size"`
|
||||
ModTime int64 `json:"mtime"`
|
||||
}
|
||||
|
||||
type AccessTable struct {
|
||||
Regex string `yaml:"regex"`
|
||||
Allow bool `yaml:"allow"`
|
||||
}
|
||||
|
||||
type UserControl struct {
|
||||
Email string
|
||||
// Access bool
|
||||
Upload bool
|
||||
Delete bool
|
||||
Token string
|
||||
}
|
||||
|
||||
type AccessConf struct {
|
||||
Upload bool `yaml:"upload" json:"upload"`
|
||||
Delete bool `yaml:"delete" json:"delete"`
|
||||
Upload bool `yaml:"upload" json:"upload"`
|
||||
Delete bool `yaml:"delete" json:"delete"`
|
||||
Users []UserControl `yaml:"users" json:"users"`
|
||||
AccessTables []AccessTable `yaml:"accessTables"`
|
||||
}
|
||||
|
||||
var reCache = make(map[string]*regexp.Regexp)
|
||||
|
||||
func (c *AccessConf) canAccess(fileName string) bool {
|
||||
for _, table := range c.AccessTables {
|
||||
pattern, ok := reCache[table.Regex]
|
||||
if !ok {
|
||||
pattern, _ = regexp.Compile(table.Regex)
|
||||
reCache[table.Regex] = pattern
|
||||
}
|
||||
// skip wrong format regex
|
||||
if pattern == nil {
|
||||
continue
|
||||
}
|
||||
if pattern.MatchString(fileName) {
|
||||
return table.Allow
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *AccessConf) canDelete(r *http.Request) bool {
|
||||
session, err := store.Get(r, defaultSessionName)
|
||||
if err != nil {
|
||||
return c.Delete
|
||||
}
|
||||
val := session.Values["user"]
|
||||
if val == nil {
|
||||
return c.Delete
|
||||
}
|
||||
userInfo := val.(*UserInfo)
|
||||
for _, rule := range c.Users {
|
||||
if rule.Email == userInfo.Email {
|
||||
return rule.Delete
|
||||
}
|
||||
}
|
||||
return c.Delete
|
||||
}
|
||||
|
||||
func (c *AccessConf) canUploadByToken(token string) bool {
|
||||
for _, rule := range c.Users {
|
||||
if rule.Token == token {
|
||||
return rule.Upload
|
||||
}
|
||||
}
|
||||
return c.Upload
|
||||
}
|
||||
|
||||
func (c *AccessConf) canUpload(r *http.Request) bool {
|
||||
token := r.FormValue("token")
|
||||
if token != "" {
|
||||
return c.canUploadByToken(token)
|
||||
}
|
||||
session, err := store.Get(r, defaultSessionName)
|
||||
if err != nil {
|
||||
return c.Upload
|
||||
}
|
||||
val := session.Values["user"]
|
||||
if val == nil {
|
||||
return c.Upload
|
||||
}
|
||||
userInfo := val.(*UserInfo)
|
||||
|
||||
for _, rule := range c.Users {
|
||||
if rule.Email == userInfo.Email {
|
||||
return rule.Upload
|
||||
}
|
||||
}
|
||||
return c.Upload
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) hJSONList(w http.ResponseWriter, r *http.Request) {
|
||||
requestPath := mux.Vars(r)["path"]
|
||||
localPath := filepath.Join(s.Root, requestPath)
|
||||
search := r.FormValue("search")
|
||||
auth := s.readAccessConf(requestPath)
|
||||
auth.Upload = auth.canUpload(r)
|
||||
auth.Delete = auth.canDelete(r)
|
||||
|
||||
// path string -> info os.FileInfo
|
||||
fileInfoMap := make(map[string]os.FileInfo, 0)
|
||||
|
@ -329,9 +597,12 @@ func (s *HTTPStaticServer) hJSONList(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
// turn file list -> json
|
||||
lrs := make([]ListResponse, 0)
|
||||
lrs := make([]HTTPFileInfo, 0)
|
||||
for path, info := range fileInfoMap {
|
||||
lr := ListResponse{
|
||||
if !auth.canAccess(info.Name()) {
|
||||
continue
|
||||
}
|
||||
lr := HTTPFileInfo{
|
||||
Name: info.Name(),
|
||||
Path: path,
|
||||
ModTime: info.ModTime().UnixNano() / 1e6,
|
||||
|
@ -348,39 +619,60 @@ func (s *HTTPStaticServer) hJSONList(w http.ResponseWriter, r *http.Request) {
|
|||
lr.Name = name
|
||||
lr.Path = filepath.Join(filepath.Dir(path), name)
|
||||
lr.Type = "dir"
|
||||
lr.Size = "-"
|
||||
lr.Size = s.historyDirSize(lr.Path)
|
||||
} else {
|
||||
lr.Type = "file"
|
||||
lr.Size = formatSize(info)
|
||||
lr.Size = info.Size() // formatSize(info)
|
||||
}
|
||||
lrs = append(lrs, lr)
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"files": lrs,
|
||||
"auth": s.readAccessConf(requestPath),
|
||||
"auth": auth,
|
||||
})
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
var dirSizeMap = make(map[string]int64)
|
||||
|
||||
func (s *HTTPStaticServer) makeIndex() error {
|
||||
var indexes = make([]IndexFileItem, 0)
|
||||
var err = filepath.Walk(s.Root, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
log.Printf("WARN: Visit path: %s error: %v", strconv.Quote(path), err)
|
||||
return filepath.SkipDir
|
||||
// return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if filepath.IsAbs(path) {
|
||||
path, _ = filepath.Rel(s.Root, path)
|
||||
}
|
||||
|
||||
path, _ = filepath.Rel(s.Root, path)
|
||||
path = filepath.ToSlash(path)
|
||||
indexes = append(indexes, IndexFileItem{path, info})
|
||||
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 {
|
||||
return size
|
||||
}
|
||||
for _, fitem := range s.indexes {
|
||||
if filepath.HasPrefix(fitem.Path, dir) {
|
||||
size += fitem.Info.Size()
|
||||
}
|
||||
}
|
||||
dirSizeMap[dir] = size
|
||||
return size
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) findIndex(text string) []IndexFileItem {
|
||||
ret := make([]IndexFileItem, 0)
|
||||
for _, item := range s.indexes {
|
||||
|
@ -410,16 +702,23 @@ func (s *HTTPStaticServer) findIndex(text string) []IndexFileItem {
|
|||
func (s *HTTPStaticServer) defaultAccessConf() AccessConf {
|
||||
return AccessConf{
|
||||
Upload: s.Upload,
|
||||
Delete: s.Delete,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) readAccessConf(requestPath string) (ac AccessConf) {
|
||||
ac = s.defaultAccessConf()
|
||||
requestPath = filepath.Clean(requestPath)
|
||||
if requestPath == "/" || requestPath == "" || requestPath == "." {
|
||||
ac = s.defaultAccessConf()
|
||||
} else {
|
||||
parentPath := filepath.Dir(requestPath)
|
||||
ac = s.readAccessConf(parentPath)
|
||||
}
|
||||
relPath := filepath.Join(s.Root, requestPath)
|
||||
if isFile(relPath) {
|
||||
relPath = filepath.Dir(relPath)
|
||||
}
|
||||
cfgFile := filepath.Join(relPath, ".ghs.yml")
|
||||
cfgFile := filepath.Join(relPath, YAMLCONF)
|
||||
data, err := ioutil.ReadFile(cfgFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
|
@ -456,3 +755,73 @@ 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 {
|
||||
panic(err)
|
||||
}
|
||||
data, err := ioutil.ReadAll(fd)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// TODO: I need to read more abouthtml/template
|
||||
var (
|
||||
funcMap template.FuncMap
|
||||
)
|
||||
|
||||
func init() {
|
||||
funcMap = template.FuncMap{
|
||||
"title": strings.Title,
|
||||
"urlhash": func(path string) string {
|
||||
httpFile, err := Assets.Open(path)
|
||||
if err != nil {
|
||||
return path + "#no-such-file"
|
||||
}
|
||||
info, err := httpFile.Stat()
|
||||
if err != nil {
|
||||
return path + "#stat-error"
|
||||
}
|
||||
return fmt.Sprintf("%s?t=%d", path, info.ModTime().Unix())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
_tmpls = make(map[string]*template.Template)
|
||||
)
|
||||
|
||||
func executeTemplate(w http.ResponseWriter, name string, v interface{}) {
|
||||
if t, ok := _tmpls[name]; ok {
|
||||
t.Execute(w, v)
|
||||
return
|
||||
}
|
||||
t := template.Must(template.New(name).Funcs(funcMap).Delims("[[", "]]").Parse(assetsContent(name)))
|
||||
_tmpls[name] = t
|
||||
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 \\/:*<>|")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
3
ipa.go
|
@ -10,7 +10,8 @@ import (
|
|||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
goplist "github.com/DHowett/go-plist"
|
||||
goplist "github.com/fork2fix/go-plist"
|
||||
//goplist "github.com/DHowett/go-plist"
|
||||
)
|
||||
|
||||
func parseIpaIcon(path string) (data []byte, err error) {
|
||||
|
|
96
main.go
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
|
@ -15,40 +16,49 @@ import (
|
|||
"text/template"
|
||||
|
||||
"github.com/alecthomas/kingpin"
|
||||
accesslog "github.com/codeskyblue/go-accesslog"
|
||||
"github.com/go-yaml/yaml"
|
||||
"github.com/goji/httpauth"
|
||||
"github.com/gorilla/handlers"
|
||||
accesslog "github.com/mash/go-accesslog"
|
||||
_ "github.com/shurcooL/vfsgen"
|
||||
)
|
||||
|
||||
type Configure struct {
|
||||
Conf *os.File `yaml:"-"`
|
||||
Addr string `yaml:"addr"`
|
||||
Root string `yaml:"root"`
|
||||
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"`
|
||||
PlistProxy string `yaml:"plistproxy"`
|
||||
Title string `yaml:"title"`
|
||||
Debug bool `yaml:"debug"`
|
||||
GoogleTrackerId string `yaml:"google-tracker-id"`
|
||||
Conf *os.File `yaml:"-"`
|
||||
Addr string `yaml:"addr"`
|
||||
Port int `yaml:"port"`
|
||||
Root string `yaml:"root"`
|
||||
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"`
|
||||
Delete bool `yaml:"delete"`
|
||||
PlistProxy string `yaml:"plistproxy"`
|
||||
Title string `yaml:"title"`
|
||||
Debug bool `yaml:"debug"`
|
||||
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
|
||||
} `yaml:"auth"`
|
||||
}
|
||||
|
||||
type logger struct {
|
||||
}
|
||||
type httpLogger struct{}
|
||||
|
||||
func (l logger) Log(record accesslog.LogRecord) {
|
||||
func (l httpLogger) Log(record accesslog.LogRecord) {
|
||||
log.Printf("%s - %s %d %s", record.Ip, record.Method, record.Status, record.Uri)
|
||||
}
|
||||
|
||||
var (
|
||||
defaultPlistProxy = "https://plistproxy.herokuapp.com/plist"
|
||||
defaultOpenID = "https://login.netease.com/openid"
|
||||
gcfg = Configure{}
|
||||
l = logger{}
|
||||
logger = httpLogger{}
|
||||
|
||||
VERSION = "unknown"
|
||||
BUILDTIME = "unknown time"
|
||||
|
@ -79,28 +89,32 @@ func versionMessage() string {
|
|||
func parseFlags() error {
|
||||
// initial default conf
|
||||
gcfg.Root = "./"
|
||||
gcfg.Addr = ":8000"
|
||||
gcfg.Port = 8000
|
||||
gcfg.Addr = ""
|
||||
gcfg.Theme = "black"
|
||||
gcfg.PlistProxy = defaultPlistProxy
|
||||
gcfg.GoogleTrackerId = "UA-81205425-2"
|
||||
gcfg.Auth.OpenID = defaultOpenID
|
||||
gcfg.Title = "Go HTTP File Server"
|
||||
|
||||
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("addr", "listen address, default :8000").Short('a').StringVar(&gcfg.Addr)
|
||||
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("httpauth", "HTTP basic auth (ex: user:pass)").StringVar(&gcfg.HttpAuth)
|
||||
kingpin.Flag("auth-type", "Auth type <http|openid>").StringVar(&gcfg.Auth.Type)
|
||||
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.Parse() // first parse conf
|
||||
|
||||
|
@ -125,12 +139,14 @@ func main() {
|
|||
data, _ := yaml.Marshal(gcfg)
|
||||
fmt.Printf("--- config ---\n%s\n", string(data))
|
||||
}
|
||||
log.SetFlags(log.Lshortfile | log.LstdFlags)
|
||||
|
||||
ss := NewHTTPStaticServer(gcfg.Root)
|
||||
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
|
||||
|
||||
if gcfg.PlistProxy != "" {
|
||||
u, err := url.Parse(gcfg.PlistProxy)
|
||||
|
@ -140,17 +156,30 @@ 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, l)
|
||||
hdlr = accesslog.NewLoggingHandler(hdlr, logger)
|
||||
|
||||
// HTTP Basic Authentication
|
||||
userpass := strings.SplitN(gcfg.HttpAuth, ":", 2)
|
||||
if len(userpass) == 2 {
|
||||
user, pass := userpass[0], userpass[1]
|
||||
hdlr = httpauth.SimpleBasicAuth(user, pass)(hdlr)
|
||||
userpass := strings.SplitN(gcfg.Auth.HTTP, ":", 2)
|
||||
switch gcfg.Auth.Type {
|
||||
case "http":
|
||||
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
|
||||
if gcfg.Cors {
|
||||
hdlr = handlers.CORS()(hdlr)
|
||||
|
@ -160,6 +189,7 @@ func main() {
|
|||
}
|
||||
|
||||
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{}{
|
||||
|
@ -168,10 +198,14 @@ func main() {
|
|||
w.Write(data)
|
||||
})
|
||||
|
||||
if gcfg.Addr == "" {
|
||||
gcfg.Addr = fmt.Sprintf(":%d", gcfg.Port)
|
||||
}
|
||||
if !strings.Contains(gcfg.Addr, ":") {
|
||||
gcfg.Addr = ":" + gcfg.Addr
|
||||
}
|
||||
log.Printf("listening on %s\n", strconv.Quote(gcfg.Addr))
|
||||
_, port, _ := net.SplitHostPort(gcfg.Addr)
|
||||
log.Printf("listening on %s, local address http://%s:%s\n", strconv.Quote(gcfg.Addr), getLocalIP(), port)
|
||||
|
||||
var err error
|
||||
if gcfg.Key != "" && gcfg.Cert != "" {
|
||||
|
|
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)
|
||||
})
|
||||
}
|
112
openid-login.go
Normal file
|
@ -0,0 +1,112 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
openid "github.com/codeskyblue/openid-go"
|
||||
"github.com/gorilla/sessions"
|
||||
)
|
||||
|
||||
var (
|
||||
nonceStore = openid.NewSimpleNonceStore()
|
||||
discoveryCache = openid.NewSimpleDiscoveryCache()
|
||||
store = sessions.NewCookieStore([]byte("something-very-secret"))
|
||||
defaultSessionName = "ghs-session"
|
||||
)
|
||||
|
||||
type UserInfo struct {
|
||||
Id string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
NickName string `json:"nickName"`
|
||||
}
|
||||
|
||||
type M map[string]interface{}
|
||||
|
||||
func init() {
|
||||
gob.Register(&UserInfo{})
|
||||
gob.Register(&M{})
|
||||
}
|
||||
|
||||
func handleOpenID(loginUrl string, secure bool) {
|
||||
http.HandleFunc("/-/login", func(w http.ResponseWriter, r *http.Request) {
|
||||
nextUrl := r.FormValue("next")
|
||||
referer := r.Referer()
|
||||
if nextUrl == "" && strings.Contains(referer, "://"+r.Host) {
|
||||
nextUrl = referer
|
||||
}
|
||||
scheme := "http"
|
||||
if r.URL.Scheme != "" {
|
||||
scheme = r.URL.Scheme
|
||||
}
|
||||
log.Println("Scheme:", scheme)
|
||||
if url, err := openid.RedirectURL(loginUrl,
|
||||
scheme+"://"+r.Host+"/-/openidcallback?next="+nextUrl, ""); err == nil {
|
||||
http.Redirect(w, r, url, 303)
|
||||
} else {
|
||||
log.Println("Should not got error here:", err)
|
||||
}
|
||||
})
|
||||
|
||||
http.HandleFunc("/-/openidcallback", func(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := openid.Verify("http://"+r.Host+r.URL.String(), discoveryCache, nonceStore)
|
||||
if err != nil {
|
||||
io.WriteString(w, "Authentication check failed.")
|
||||
return
|
||||
}
|
||||
session, err := store.Get(r, defaultSessionName)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
user := &UserInfo{
|
||||
Id: id,
|
||||
Email: r.FormValue("openid.sreg.email"),
|
||||
Name: r.FormValue("openid.sreg.fullname"),
|
||||
NickName: r.FormValue("openid.sreg.nickname"),
|
||||
}
|
||||
session.Values["user"] = user
|
||||
if err := session.Save(r, w); err != nil {
|
||||
log.Println("session save error:", err)
|
||||
}
|
||||
|
||||
nextUrl := r.FormValue("next")
|
||||
if nextUrl == "" {
|
||||
nextUrl = "/"
|
||||
}
|
||||
http.Redirect(w, r, nextUrl, 302)
|
||||
})
|
||||
|
||||
http.HandleFunc("/-/user", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, err := store.Get(r, defaultSessionName)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
val := session.Values["user"]
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
data, _ := json.Marshal(val)
|
||||
w.Write(data)
|
||||
})
|
||||
|
||||
http.HandleFunc("/-/logout", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, err := store.Get(r, defaultSessionName)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
delete(session.Values, "user")
|
||||
session.Options.MaxAge = -1
|
||||
nextUrl := r.FormValue("next")
|
||||
_ = session.Save(r, w)
|
||||
if nextUrl == "" {
|
||||
nextUrl = r.Referer()
|
||||
}
|
||||
http.Redirect(w, r, nextUrl, 302)
|
||||
})
|
||||
}
|
|
@ -1,219 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<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="/-/res/favicon.png" />
|
||||
<link rel="stylesheet" type="text/css" href="/-/res/bootstrap-3.3.5/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/-/res/font-awesome-4.6.3/css/font-awesome.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/-/res/css/github-markdown.css">
|
||||
<link rel="stylesheet" type="text/css" href="/-/res/css/dropzone.css">
|
||||
<link rel="stylesheet" type="text/css" href="/-/res/css/scrollUp-image.css">
|
||||
<link rel="stylesheet" type="text/css" href="/-/res/css/style.css">
|
||||
<link rel="stylesheet" type="text/css" href="/-/res/themes/[[.Theme]].css">
|
||||
</head>
|
||||
|
||||
<body id="app">
|
||||
<nav class="navbar navbar-default">
|
||||
<div class="container">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-2">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="/">[[.Title]]</a>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-2">
|
||||
<ul class="nav navbar-nav">
|
||||
<li class="hidden-xs">
|
||||
<a href="javascript:void(0)" v-on:click='genQrcode("/", location.origin)'>
|
||||
View in Phone
|
||||
<span class="glyphicon glyphicon-qrcode"></span>
|
||||
</a>
|
||||
</li>
|
||||
</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">
|
||||
<span class="glyphicon glyphicon-search"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
<ul id="nav-right-bar" class="nav navbar-nav navbar-right">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container">
|
||||
<div class="col-md-12">
|
||||
<ol class="breadcrumb">
|
||||
<li>
|
||||
<a v-on:click='changePath("/", $event)' href="/"><i class="fa fa-home"></i></a>
|
||||
</li>
|
||||
<li v-for="bc in breadcrumb.slice(0, breadcrumb.length-1)">
|
||||
<a v-on:click='changePath(bc.path, $event)' href="{{bc.path}}">{{bc.name}}</a>
|
||||
</li>
|
||||
<li v-if="breadcrumb.length >= 1">
|
||||
{{breadcrumb.slice(-1)[0].name}}
|
||||
</li>
|
||||
</ol>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<td colspan=4>
|
||||
<!-- <button class="btn btn-xs btn-default" v-on:click='toggleHidden()'>
|
||||
Back <i class="fa" v-bind:class='showHidden ? "fa-eye" : "fa-eye-slash"'></i>
|
||||
</button> -->
|
||||
<button class="btn btn-xs btn-default" v-on:click='toggleHidden()'>
|
||||
Hidden <i class="fa" v-bind:class='showHidden ? "fa-eye" : "fa-eye-slash"'></i>
|
||||
</button>
|
||||
<button class="btn btn-xs btn-default" v-if="auth.upload" data-toggle="modal" data-target="#upload-modal">
|
||||
Upload <i class="fa fa-upload"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Size</th>
|
||||
<th class="hidden-xs">
|
||||
<span style="cursor: pointer" v-on:click='mtimeTypeFromNow = !mtimeTypeFromNow'>ModTime</span>
|
||||
</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="f in computedFiles">
|
||||
<td>
|
||||
<a v-on:click='clickFileOrDir(f, $event)' href="/{{f.path}}">
|
||||
<i style="padding-right: 0.5em" class="fa" v-bind:class='genFileClass(f)'></i> {{f.name}}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{f.size}}</td>
|
||||
<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}}">
|
||||
<span class="hidden-xs">Archive</span> Zip
|
||||
<span class="glyphicon glyphicon-download-alt"></span>
|
||||
</a>
|
||||
</template>
|
||||
<template v-if="f.type == 'file'">
|
||||
<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>
|
||||
<a class="btn btn-default btn-xs hidden-xs" v-on:click="genQrcode(f.name)" href="javascript:void(0)">
|
||||
<span v-if="shouldHaveQrcode(f.name)">QRCode</span>
|
||||
<span class="glyphicon glyphicon-qrcode"></span>
|
||||
</a>
|
||||
<a class="btn btn-default btn-xs visible-xs" v-if="shouldHaveQrcode(f.name)" href="{{genInstallURL(f.name)}}">
|
||||
Install <i class="fa fa-cube"></i>
|
||||
</a>
|
||||
<a class="btn btn-default btn-xs" v-if="auth.delete" v-on:click="deletePathConfirm(f, $event)" href="javascript:void(0)">
|
||||
<span style="color:#CC3300" class="glyphicon glyphicon-trash"></span>
|
||||
</a>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-12" id="preview" v-if="previewFile">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title" style="font-weight: normal">
|
||||
<i class="fa" v-bind:class='genFileClass(previewFile)'></i>
|
||||
{{previewFile.name}}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<article class="markdown-body">{{{previewFile.contentHTML }}}
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12" id="content">
|
||||
<!-- Small qrcode modal -->
|
||||
<div id="qrcode-modal" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title">
|
||||
<span id="qrcode-title"></span>
|
||||
<a style="font-size: 0.6em" href="#" id="qrcode-link">[view]</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="qrcodeCanvas"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Upload modal-->
|
||||
<div id="upload-modal" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title">
|
||||
<i class="fa fa-upload"></i> File upload
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form action="#" class="dropzone" id="my-dropzone"></form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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>. 2016. go1.6
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/-/res/js/jquery-3.1.0.min.js"></script>
|
||||
<script src="/-/res/js/jquery.qrcode.js"></script>
|
||||
<script src="/-/res/js/jquery.scrollUp.min.js"></script>
|
||||
<script src="/-/res/js/qrcode.js"></script>
|
||||
<script src="/-/res/js/vue-1.0.min.js"></script>
|
||||
<script src="/-/res/js/showdown-1.4.2.min.js"></script>
|
||||
<script src="/-/res/js/moment.min.js"></script>
|
||||
<script src="/-/res/js/dropzone.js"></script>
|
||||
<script src="/-/res/js/underscore-min.js"></script>
|
||||
<script src="/-/res/bootstrap-3.3.5/js/bootstrap.min.js"></script>
|
||||
<script src="/-/res/js/index.js"></script>
|
||||
[[if .GoogleTrackerId ]]
|
||||
<script>
|
||||
(function(i, s, o, g, r, a, m) {
|
||||
i['GoogleAnalyticsObject'] = r;
|
||||
i[r] = i[r] || function() {
|
||||
(i[r].q = i[r].q || []).push(arguments)
|
||||
}, i[r].l = 1 * new Date();
|
||||
a = s.createElement(o),
|
||||
m = s.getElementsByTagName(o)[0];
|
||||
a.async = 1;
|
||||
a.src = g;
|
||||
m.parentNode.insertBefore(a, m)
|
||||
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
|
||||
|
||||
ga('create', '[[.GoogleTrackerId]]', 'auto');
|
||||
ga('send', 'pageview');
|
||||
</script>
|
||||
[[ end ]]
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,71 +0,0 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<title>[[.Name]] install</title>
|
||||
<meta http-equiv="Content-Type" content="text/HTML; charset=utf-8">
|
||||
<meta content="target-densitydpi=device-dpi,width=640" name="viewport" id="viewport">
|
||||
<link rel="shortcut icon" type="image/png" href="/-/res/favicon.png" />
|
||||
<script type="text/javascript" src="/-/res/js/ua-parser.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
function showById(name) {
|
||||
document.getElementById(name).style.display = 'block';
|
||||
}
|
||||
|
||||
function checkBrowerAndDownload() {
|
||||
var parser = new UAParser();
|
||||
var os_info = parser.getOS();
|
||||
console.log(os_info)
|
||||
|
||||
if (navigator.userAgent.toLowerCase().match(/MicroMessenger/i) == "micromessenger") {
|
||||
showById('wechat');
|
||||
return;
|
||||
}
|
||||
var plistLink = "[[.PlistLink]]";
|
||||
var ipaInstallLink = 'itms-services://?action=download-manifest&url=' + plistLink;
|
||||
|
||||
document.getElementById('itms-link').href = ipaInstallLink;
|
||||
|
||||
if (os_info.name == 'Android') {
|
||||
return;
|
||||
} else if (os_info.name == 'iOS') {
|
||||
showById('safari');
|
||||
|
||||
location.href = ipaInstallLink;
|
||||
return;
|
||||
} else {
|
||||
showById('browser');
|
||||
return;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<style>
|
||||
#wechat {
|
||||
position: relative;
|
||||
width: 640px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
min-height: 777px;
|
||||
}
|
||||
</style>
|
||||
<div id="wechat" style="display: none">
|
||||
<img style='width: 100%;position: relative;' src='/-/res/imgs/wx.png' />
|
||||
</div>
|
||||
<div id="browser" style="display: none">
|
||||
This is IPA install page, you should open this link with your iPhone.
|
||||
</div>
|
||||
<div id="safari" style="display: none">
|
||||
If install not started soon, click <a id="itms-link" href="#">here</a>
|
||||
</div>
|
||||
<div id="android" style="display: none">
|
||||
This is IPA install page, not for android.
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
checkBrowerAndDownload();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
271
res/js/index.js
|
@ -1,271 +0,0 @@
|
|||
jQuery('#qrcodeCanvas').qrcode({
|
||||
text: "http://jetienne.com/"
|
||||
});
|
||||
|
||||
function getExtention(fname) {
|
||||
return fname.slice((fname.lastIndexOf(".") - 1 >>> 0) + 2);
|
||||
}
|
||||
|
||||
function pathJoin(parts, sep) {
|
||||
var separator = sep || '/';
|
||||
var replace = new RegExp(separator + '{1,}', 'g');
|
||||
return parts.join(separator).replace(replace, separator);
|
||||
}
|
||||
|
||||
function getQueryString(name) {
|
||||
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
|
||||
var r = decodeURI(window.location.search).substr(1).match(reg);
|
||||
if (r != null) return r[2].replace(/\+/g, ' ');
|
||||
return null;
|
||||
}
|
||||
|
||||
var vm = new Vue({
|
||||
el: "#app",
|
||||
data: {
|
||||
message: "Hello vue.js",
|
||||
location: window.location,
|
||||
breadcrumb: [],
|
||||
showHidden: false,
|
||||
previewFile: null,
|
||||
version: "loading",
|
||||
mtimeTypeFromNow: false, // or fromNow
|
||||
auth: {},
|
||||
search: getQueryString("search"),
|
||||
files: [{
|
||||
name: "loading ...",
|
||||
path: "",
|
||||
size: "...",
|
||||
type: "dir",
|
||||
}]
|
||||
},
|
||||
computed: {
|
||||
computedFiles: function() {
|
||||
var that = this;
|
||||
this.previewFile = null;
|
||||
|
||||
var files = this.files.filter(function(f) {
|
||||
if (f.name == 'README.md') {
|
||||
that.previewFile = {
|
||||
name: f.name,
|
||||
path: f.path,
|
||||
size: f.size,
|
||||
type: 'markdown',
|
||||
contentHTML: '',
|
||||
}
|
||||
}
|
||||
if (!that.showHidden && f.name.slice(0, 1) === '.') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// console.log(this.previewFile)
|
||||
if (this.previewFile) {
|
||||
var name = this.previewFile.name; // For now only README.md
|
||||
console.log(pathJoin([location.pathname, 'README.md']))
|
||||
$.ajax({
|
||||
url: pathJoin([location.pathname, 'README.md']),
|
||||
method: 'GET',
|
||||
success: function(res) {
|
||||
var converter = new showdown.Converter({
|
||||
tables: true,
|
||||
omitExtraWLInCodeBlocks: true,
|
||||
parseImgDimensions: true,
|
||||
simplifiedAutoLink: true,
|
||||
literalMidWordUnderscores: true,
|
||||
tasklists: true,
|
||||
ghCodeBlocks: true,
|
||||
smoothLivePreview: true,
|
||||
});
|
||||
|
||||
var html = converter.makeHtml(res);
|
||||
that.previewFile.contentHTML = html;
|
||||
},
|
||||
error: function(err) {
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return files;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatTime: function(timestamp) {
|
||||
var m = moment(timestamp);
|
||||
if (this.mtimeTypeFromNow) {
|
||||
return m.fromNow();
|
||||
}
|
||||
return m.format('YYYY-MM-DD HH:mm:ss');
|
||||
},
|
||||
toggleHidden: function() {
|
||||
this.showHidden = !this.showHidden;
|
||||
},
|
||||
genInstallURL: function(name) {
|
||||
if (getExtention(name) == "ipa") {
|
||||
urlPath = location.protocol + "//" + pathJoin([location.host, "/-/ipa/link", location.pathname, name]);
|
||||
return urlPath;
|
||||
}
|
||||
return location.protocol + "//" + pathJoin([location.host, location.pathname, name]);
|
||||
},
|
||||
genQrcode: function(text, title) {
|
||||
var urlPath = this.genInstallURL(text);
|
||||
$("#qrcode-title").html(title || text);
|
||||
$("#qrcode-link").attr("href", urlPath);
|
||||
$('#qrcodeCanvas').empty().qrcode({
|
||||
text: urlPath
|
||||
});
|
||||
$("#qrcode-modal").modal("show");
|
||||
},
|
||||
shouldHaveQrcode: function(name) {
|
||||
return ['apk', 'ipa'].indexOf(getExtention(name)) !== -1;
|
||||
},
|
||||
genFileClass: function(f) {
|
||||
if (f.type == "dir") {
|
||||
if (f.name == '.git') {
|
||||
return 'fa-git-square';
|
||||
}
|
||||
return "fa-folder-open";
|
||||
}
|
||||
var ext = getExtention(f.name);
|
||||
switch (ext) {
|
||||
case "go":
|
||||
case "py":
|
||||
case "js":
|
||||
case "java":
|
||||
case "c":
|
||||
case "cpp":
|
||||
case "h":
|
||||
return "fa-file-code-o";
|
||||
case "pdf":
|
||||
return "fa-file-pdf-o";
|
||||
case "zip":
|
||||
return "fa-file-zip-o";
|
||||
case "mp3":
|
||||
case "wav":
|
||||
return "fa-file-audio-o";
|
||||
case "jpg":
|
||||
case "png":
|
||||
case "gif":
|
||||
case "jpeg":
|
||||
case "tiff":
|
||||
return "fa-file-picture-o";
|
||||
case "ipa":
|
||||
case "dmg":
|
||||
return "fa-apple";
|
||||
case "apk":
|
||||
return "fa-android";
|
||||
case "exe":
|
||||
return "fa-windows";
|
||||
}
|
||||
return "fa-file-text-o"
|
||||
},
|
||||
clickFileOrDir: function(f, e) {
|
||||
if (f.type == "file") {
|
||||
return true;
|
||||
}
|
||||
var reqPath = pathJoin([location.pathname, f.name]);
|
||||
loadDirectory(reqPath);
|
||||
e.preventDefault()
|
||||
},
|
||||
changePath: function(reqPath, e) {
|
||||
loadDirectory(reqPath);
|
||||
e.preventDefault()
|
||||
},
|
||||
deletePathConfirm: function(f, e) {
|
||||
// confirm
|
||||
e.preventDefault();
|
||||
$.ajax({
|
||||
url: pathJoin([location.pathname, f.name]),
|
||||
method: 'DELETE',
|
||||
success: function(res) {
|
||||
loadFileList()
|
||||
},
|
||||
error: function(err) {
|
||||
alert(err.responseText);
|
||||
}
|
||||
});
|
||||
},
|
||||
updateBreadcrumb: function() {
|
||||
var pathname = decodeURI(location.pathname || "/");
|
||||
var parts = pathname.split('/');
|
||||
this.breadcrumb = [];
|
||||
if (pathname == "/") {
|
||||
return this.breadcrumb;
|
||||
}
|
||||
var i = 2;
|
||||
for (; i <= parts.length; i += 1) {
|
||||
var name = parts[i - 1];
|
||||
var path = parts.slice(0, i).join('/');
|
||||
this.breadcrumb.push({
|
||||
name: name + (i == parts.length ? ' /' : ''),
|
||||
path: path
|
||||
})
|
||||
}
|
||||
return this.breadcrumb;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
window.onpopstate = function(event) {
|
||||
var pathname = decodeURI(location.pathname)
|
||||
loadFileList()
|
||||
}
|
||||
|
||||
function loadDirectory(reqPath) {
|
||||
window.history.pushState({}, "", reqPath);
|
||||
loadFileList(reqPath)
|
||||
}
|
||||
|
||||
function loadFileList(pathname) {
|
||||
var pathname = pathname || location.pathname;
|
||||
// console.log("load filelist:", pathname)
|
||||
$.ajax({
|
||||
url: pathJoin(["/-/json", pathname]),
|
||||
dataType: "json",
|
||||
cache: false,
|
||||
success: function(res) {
|
||||
res.files = _.sortBy(res.files, function(f) {
|
||||
return [f.type, f.name];
|
||||
})
|
||||
|
||||
vm.files = res.files;
|
||||
vm.auth = res.auth;
|
||||
},
|
||||
error: function(err) {
|
||||
console.error(err)
|
||||
},
|
||||
});
|
||||
vm.updateBreadcrumb();
|
||||
}
|
||||
|
||||
// For page first loading
|
||||
loadFileList(location.pathname + location.search)
|
||||
|
||||
// update version
|
||||
$.getJSON("/-/sysinfo", function(res) {
|
||||
vm.version = res.version;
|
||||
})
|
||||
|
||||
Dropzone.options.myDropzone = {
|
||||
paramName: "file",
|
||||
maxFilesize: 1024,
|
||||
addRemoveLinks: true,
|
||||
init: function() {
|
||||
this.on("uploadprogress", function(file, progress) {
|
||||
console.log("File progress", progress);
|
||||
});
|
||||
this.on("complete", function(file) {
|
||||
console.log("reload file list")
|
||||
loadFileList()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
$(function() {
|
||||
$.scrollUp({
|
||||
scrollText: '', // text are defined in css
|
||||
});
|
||||
});
|
||||
Vue.filter('fromNow', function(value) {
|
||||
return moment(value).fromNow();
|
||||
})
|
4
res/js/showdown-1.4.2.min.js
vendored
|
@ -1,20 +0,0 @@
|
|||
// +build bindata
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func init() {
|
||||
http.Handle("/-/res/", http.StripPrefix("/-/res/", http.FileServer(assetFS())))
|
||||
|
||||
for name, path := range templates {
|
||||
data, err := Asset(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
ParseTemplate(name, string(data))
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
// +build !bindata
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func init() {
|
||||
//selfDir := filepath.Dir(os.Args[0])
|
||||
//resDir := filepath.Join(selfDir, "./res")
|
||||
resDir := "./res"
|
||||
http.Handle("/-/res/", http.StripPrefix("/-/res/", http.FileServer(http.Dir(resDir))))
|
||||
|
||||
for name, path := range templates {
|
||||
content, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
ParseTemplate(name, string(content))
|
||||
}
|
||||
}
|
5
testdata/deletable/.ghs.yml
vendored
|
@ -1,2 +1,7 @@
|
|||
upload: true
|
||||
delete: true
|
||||
accessTables:
|
||||
- regex: block.file
|
||||
allow: false
|
||||
- regex: visual.file
|
||||
allow: true
|
0
testdata/deletable/block.file
vendored
Normal file
0
testdata/deletable/other.file
vendored
Normal file
0
testdata/deletable/visual.file
vendored
Normal file
7
testdata/uploadable/.ghs.yml
vendored
|
@ -1,2 +1,7 @@
|
|||
---
|
||||
upload: true
|
||||
upload: false
|
||||
users:
|
||||
- email: "user@example.com"
|
||||
upload: true
|
||||
delete: true
|
||||
token: 123456
|
0
testdata/uploadable/sub-upload/.gitkeep
vendored
Normal file
58
utils.go
|
@ -1,28 +1,27 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func formatSize(file os.FileInfo) string {
|
||||
if file.IsDir() {
|
||||
return "-"
|
||||
}
|
||||
size := file.Size()
|
||||
switch {
|
||||
case size > 1024*1024:
|
||||
return fmt.Sprintf("%.1f MB", float64(size)/1024/1024)
|
||||
case size > 1024:
|
||||
return fmt.Sprintf("%.1f KB", float64(size)/1024)
|
||||
default:
|
||||
return strconv.Itoa(int(size)) + " B"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
// func formatSize(file os.FileInfo) string {
|
||||
// if file.IsDir() {
|
||||
// return "-"
|
||||
// }
|
||||
// size := file.Size()
|
||||
// switch {
|
||||
// case size > 1024*1024:
|
||||
// return fmt.Sprintf("%.1f MB", float64(size)/1024/1024)
|
||||
// case size > 1024:
|
||||
// return fmt.Sprintf("%.1f KB", float64(size)/1024)
|
||||
// default:
|
||||
// return strconv.Itoa(int(size)) + " B"
|
||||
// }
|
||||
// return ""
|
||||
// }
|
||||
|
||||
func getRealIP(req *http.Request) string {
|
||||
xip := req.Header.Get("X-Real-IP")
|
||||
|
@ -56,3 +55,28 @@ func SublimeContains(s, substr string) bool {
|
|||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
// getLocalIP returns the non loopback local IP of the host
|
||||
func getLocalIP() string {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, address := range addrs {
|
||||
// check the address type and if it is not a loopback the display it
|
||||
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||
if ipnet.IP.To4() != nil {
|
||||
return ipnet.IP.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !info.IsDir()
|
||||
}
|
||||
|
|
58
vendor/github.com/DHowett/go-plist/LICENSE
generated
vendored
|
@ -1,58 +0,0 @@
|
|||
Copyright (c) 2013, Dustin L. Howett. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
The views and conclusions contained in the software and documentation are those
|
||||
of the authors and should not be interpreted as representing official policies,
|
||||
either expressed or implied, of the FreeBSD Project.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Parts of this package were made available under the license covering
|
||||
the Go language and all attended core libraries. That license follows.
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Copyright (c) 2012 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
19
vendor/github.com/DHowett/go-plist/README.md
generated
vendored
|
@ -1,19 +0,0 @@
|
|||
# plist - A pure Go property list transcoder
|
||||
## INSTALL
|
||||
$ go get howett.net/plist
|
||||
|
||||
## FEATURES
|
||||
* Supports encoding/decoding property lists (Apple XML, Apple Binary, OpenStep and GNUStep) from/to arbitrary Go types
|
||||
|
||||
## USE
|
||||
```go
|
||||
package main
|
||||
import (
|
||||
"howett.net/plist"
|
||||
"os"
|
||||
)
|
||||
func main() {
|
||||
encoder := plist.NewEncoder(os.Stdout)
|
||||
encoder.Encode(map[string]string{"hello": "world"})
|
||||
}
|
||||
```
|
546
vendor/github.com/DHowett/go-plist/bplist.go
generated
vendored
|
@ -1,546 +0,0 @@
|
|||
package plist
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"math"
|
||||
"runtime"
|
||||
"time"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
type bplistTrailer struct {
|
||||
Unused [5]uint8
|
||||
SortVersion uint8
|
||||
OffsetIntSize uint8
|
||||
ObjectRefSize uint8
|
||||
NumObjects uint64
|
||||
TopObject uint64
|
||||
OffsetTableOffset uint64
|
||||
}
|
||||
|
||||
const (
|
||||
bpTagNull uint8 = 0x00
|
||||
bpTagBoolFalse = 0x08
|
||||
bpTagBoolTrue = 0x09
|
||||
bpTagInteger = 0x10
|
||||
bpTagReal = 0x20
|
||||
bpTagDate = 0x30
|
||||
bpTagData = 0x40
|
||||
bpTagASCIIString = 0x50
|
||||
bpTagUTF16String = 0x60
|
||||
bpTagUID = 0x80
|
||||
bpTagArray = 0xA0
|
||||
bpTagDictionary = 0xD0
|
||||
)
|
||||
|
||||
type bplistGenerator struct {
|
||||
writer *countedWriter
|
||||
uniqmap map[interface{}]uint64
|
||||
objmap map[*plistValue]uint64
|
||||
objtable []*plistValue
|
||||
nobjects uint64
|
||||
trailer bplistTrailer
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) flattenPlistValue(pval *plistValue) {
|
||||
switch pval.kind {
|
||||
case String, Integer, Real:
|
||||
if _, ok := p.uniqmap[pval.value]; ok {
|
||||
return
|
||||
}
|
||||
p.uniqmap[pval.value] = p.nobjects
|
||||
case Date:
|
||||
k := pval.value.(time.Time).UnixNano()
|
||||
if _, ok := p.uniqmap[k]; ok {
|
||||
return
|
||||
}
|
||||
p.uniqmap[k] = p.nobjects
|
||||
case Data:
|
||||
// Data are uniqued by their checksums.
|
||||
// The wonderful difference between uint64 (which we use for numbers)
|
||||
// and uint32 makes this possible.
|
||||
// Todo: Look at calculating this only once and storing it somewhere;
|
||||
// crc32 is fairly quick, however.
|
||||
uniqkey := crc32.ChecksumIEEE(pval.value.([]byte))
|
||||
if _, ok := p.uniqmap[uniqkey]; ok {
|
||||
return
|
||||
}
|
||||
p.uniqmap[uniqkey] = p.nobjects
|
||||
}
|
||||
|
||||
p.objtable = append(p.objtable, pval)
|
||||
p.objmap[pval] = p.nobjects
|
||||
p.nobjects++
|
||||
|
||||
switch pval.kind {
|
||||
case Dictionary:
|
||||
dict := pval.value.(*dictionary)
|
||||
dict.populateArrays()
|
||||
for _, k := range dict.keys {
|
||||
p.flattenPlistValue(&plistValue{String, k})
|
||||
}
|
||||
for _, v := range dict.values {
|
||||
p.flattenPlistValue(v)
|
||||
}
|
||||
case Array:
|
||||
subvalues := pval.value.([]*plistValue)
|
||||
for _, v := range subvalues {
|
||||
p.flattenPlistValue(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) indexForPlistValue(pval *plistValue) (uint64, bool) {
|
||||
var v uint64
|
||||
var ok bool
|
||||
switch pval.kind {
|
||||
case String, Integer, Real:
|
||||
v, ok = p.uniqmap[pval.value]
|
||||
case Date:
|
||||
v, ok = p.uniqmap[pval.value.(time.Time).UnixNano()]
|
||||
case Data:
|
||||
v, ok = p.uniqmap[crc32.ChecksumIEEE(pval.value.([]byte))]
|
||||
default:
|
||||
v, ok = p.objmap[pval]
|
||||
}
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) generateDocument(rootpval *plistValue) {
|
||||
p.objtable = make([]*plistValue, 0, 15)
|
||||
p.uniqmap = make(map[interface{}]uint64)
|
||||
p.objmap = make(map[*plistValue]uint64)
|
||||
p.flattenPlistValue(rootpval)
|
||||
|
||||
p.trailer.NumObjects = uint64(len(p.objtable))
|
||||
p.trailer.ObjectRefSize = uint8(minimumSizeForInt(p.trailer.NumObjects))
|
||||
|
||||
p.writer.Write([]byte("bplist00"))
|
||||
|
||||
offtable := make([]uint64, p.trailer.NumObjects)
|
||||
for i, pval := range p.objtable {
|
||||
offtable[i] = uint64(p.writer.BytesWritten())
|
||||
p.writePlistValue(pval)
|
||||
}
|
||||
|
||||
p.trailer.OffsetIntSize = uint8(minimumSizeForInt(uint64(p.writer.BytesWritten())))
|
||||
p.trailer.TopObject = p.objmap[rootpval]
|
||||
p.trailer.OffsetTableOffset = uint64(p.writer.BytesWritten())
|
||||
|
||||
for _, offset := range offtable {
|
||||
p.writeSizedInt(offset, int(p.trailer.OffsetIntSize))
|
||||
}
|
||||
|
||||
binary.Write(p.writer, binary.BigEndian, p.trailer)
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writePlistValue(pval *plistValue) {
|
||||
if pval == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch pval.kind {
|
||||
case Dictionary:
|
||||
p.writeDictionaryTag(pval.value.(*dictionary))
|
||||
case Array:
|
||||
p.writeArrayTag(pval.value.([]*plistValue))
|
||||
case String:
|
||||
p.writeStringTag(pval.value.(string))
|
||||
case Integer:
|
||||
p.writeIntTag(pval.value.(signedInt).value)
|
||||
case Real:
|
||||
p.writeRealTag(pval.value.(sizedFloat).value, pval.value.(sizedFloat).bits)
|
||||
case Boolean:
|
||||
p.writeBoolTag(pval.value.(bool))
|
||||
case Data:
|
||||
p.writeDataTag(pval.value.([]byte))
|
||||
case Date:
|
||||
p.writeDateTag(pval.value.(time.Time))
|
||||
}
|
||||
}
|
||||
|
||||
func minimumSizeForInt(n uint64) int {
|
||||
switch {
|
||||
case n <= uint64(0xff):
|
||||
return 1
|
||||
case n <= uint64(0xffff):
|
||||
return 2
|
||||
case n <= uint64(0xffffffff):
|
||||
return 4
|
||||
default:
|
||||
return 8
|
||||
}
|
||||
panic(errors.New("illegal integer size"))
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeSizedInt(n uint64, nbytes int) {
|
||||
var val interface{}
|
||||
switch nbytes {
|
||||
case 1:
|
||||
val = uint8(n)
|
||||
case 2:
|
||||
val = uint16(n)
|
||||
case 4:
|
||||
val = uint32(n)
|
||||
case 8:
|
||||
val = n
|
||||
default:
|
||||
panic(errors.New("illegal integer size"))
|
||||
}
|
||||
binary.Write(p.writer, binary.BigEndian, val)
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeBoolTag(v bool) {
|
||||
tag := uint8(bpTagBoolFalse)
|
||||
if v {
|
||||
tag = bpTagBoolTrue
|
||||
}
|
||||
binary.Write(p.writer, binary.BigEndian, tag)
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeIntTag(n uint64) {
|
||||
var tag uint8
|
||||
var val interface{}
|
||||
switch {
|
||||
case n <= uint64(0xff):
|
||||
val = uint8(n)
|
||||
tag = bpTagInteger | 0x0
|
||||
case n <= uint64(0xffff):
|
||||
val = uint16(n)
|
||||
tag = bpTagInteger | 0x1
|
||||
case n <= uint64(0xffffffff):
|
||||
val = uint32(n)
|
||||
tag = bpTagInteger | 0x2
|
||||
default:
|
||||
val = n
|
||||
tag = bpTagInteger | 0x3
|
||||
}
|
||||
|
||||
binary.Write(p.writer, binary.BigEndian, tag)
|
||||
binary.Write(p.writer, binary.BigEndian, val)
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeRealTag(n float64, bits int) {
|
||||
var tag uint8 = bpTagReal | 0x3
|
||||
var val interface{} = n
|
||||
if bits == 32 {
|
||||
val = float32(n)
|
||||
tag = bpTagReal | 0x2
|
||||
}
|
||||
|
||||
binary.Write(p.writer, binary.BigEndian, tag)
|
||||
binary.Write(p.writer, binary.BigEndian, val)
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeDateTag(t time.Time) {
|
||||
tag := uint8(bpTagDate) | 0x3
|
||||
val := float64(t.In(time.UTC).UnixNano()) / float64(time.Second)
|
||||
val -= 978307200 // Adjust to Apple Epoch
|
||||
|
||||
binary.Write(p.writer, binary.BigEndian, tag)
|
||||
binary.Write(p.writer, binary.BigEndian, val)
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeCountedTag(tag uint8, count uint64) {
|
||||
marker := tag
|
||||
if count >= 0xF {
|
||||
marker |= 0xF
|
||||
} else {
|
||||
marker |= uint8(count)
|
||||
}
|
||||
|
||||
binary.Write(p.writer, binary.BigEndian, marker)
|
||||
|
||||
if count >= 0xF {
|
||||
p.writeIntTag(count)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeDataTag(data []byte) {
|
||||
p.writeCountedTag(bpTagData, uint64(len(data)))
|
||||
binary.Write(p.writer, binary.BigEndian, data)
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeStringTag(str string) {
|
||||
for _, r := range str {
|
||||
if r > 0xFF {
|
||||
utf16Runes := utf16.Encode([]rune(str))
|
||||
p.writeCountedTag(bpTagUTF16String, uint64(len(utf16Runes)))
|
||||
binary.Write(p.writer, binary.BigEndian, utf16Runes)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
p.writeCountedTag(bpTagASCIIString, uint64(len(str)))
|
||||
binary.Write(p.writer, binary.BigEndian, []byte(str))
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeDictionaryTag(dict *dictionary) {
|
||||
p.writeCountedTag(bpTagDictionary, uint64(dict.count))
|
||||
vals := make([]uint64, dict.count*2)
|
||||
cnt := dict.count
|
||||
for i, k := range dict.keys {
|
||||
keyIdx, ok := p.uniqmap[k]
|
||||
if !ok {
|
||||
panic(errors.New("failed to find key " + k + " in object map during serialization"))
|
||||
}
|
||||
vals[i] = keyIdx
|
||||
}
|
||||
for i, v := range dict.values {
|
||||
objIdx, ok := p.indexForPlistValue(v)
|
||||
if !ok {
|
||||
panic(errors.New("failed to find value in object map during serialization"))
|
||||
}
|
||||
vals[i+cnt] = objIdx
|
||||
}
|
||||
|
||||
for _, v := range vals {
|
||||
p.writeSizedInt(v, int(p.trailer.ObjectRefSize))
|
||||
}
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeArrayTag(arr []*plistValue) {
|
||||
p.writeCountedTag(bpTagArray, uint64(len(arr)))
|
||||
for _, v := range arr {
|
||||
objIdx, ok := p.indexForPlistValue(v)
|
||||
if !ok {
|
||||
panic(errors.New("failed to find value in object map during serialization"))
|
||||
}
|
||||
|
||||
p.writeSizedInt(objIdx, int(p.trailer.ObjectRefSize))
|
||||
}
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) Indent(i string) {
|
||||
// There's nothing to indent.
|
||||
}
|
||||
|
||||
func newBplistGenerator(w io.Writer) *bplistGenerator {
|
||||
return &bplistGenerator{
|
||||
writer: &countedWriter{Writer: mustWriter{w}},
|
||||
}
|
||||
}
|
||||
|
||||
type bplistParser struct {
|
||||
reader io.ReadSeeker
|
||||
version int
|
||||
buf []byte
|
||||
objrefs map[uint64]*plistValue
|
||||
offtable []uint64
|
||||
trailer bplistTrailer
|
||||
}
|
||||
|
||||
func (p *bplistParser) parseDocument() (pval *plistValue, parseError error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if _, ok := r.(runtime.Error); ok {
|
||||
panic(r)
|
||||
}
|
||||
if _, ok := r.(invalidPlistError); ok {
|
||||
parseError = r.(error)
|
||||
} else {
|
||||
// Wrap all non-invalid-plist errors.
|
||||
parseError = plistParseError{"binary", r.(error)}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
magic := make([]byte, 6)
|
||||
ver := make([]byte, 2)
|
||||
p.reader.Seek(0, 0)
|
||||
p.reader.Read(magic)
|
||||
if !bytes.Equal(magic, []byte("bplist")) {
|
||||
panic(invalidPlistError{"binary", errors.New("mismatched magic")})
|
||||
}
|
||||
|
||||
_, err := p.reader.Read(ver)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
p.version = int(mustParseInt(string(ver), 10, 0))
|
||||
|
||||
if p.version > 1 {
|
||||
panic(fmt.Errorf("unexpected version %d", p.version))
|
||||
}
|
||||
|
||||
p.objrefs = make(map[uint64]*plistValue)
|
||||
_, err = p.reader.Seek(-32, 2)
|
||||
if err != nil && err != io.EOF {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = binary.Read(p.reader, binary.BigEndian, &p.trailer)
|
||||
if err != nil && err != io.EOF {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
p.offtable = make([]uint64, p.trailer.NumObjects)
|
||||
|
||||
// SEEK_SET
|
||||
_, err = p.reader.Seek(int64(p.trailer.OffsetTableOffset), 0)
|
||||
if err != nil && err != io.EOF {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for i := uint64(0); i < p.trailer.NumObjects; i++ {
|
||||
off := p.readSizedInt(int(p.trailer.OffsetIntSize))
|
||||
p.offtable[i] = off
|
||||
}
|
||||
|
||||
for _, off := range p.offtable {
|
||||
p.valueAtOffset(off)
|
||||
}
|
||||
|
||||
pval = p.valueAtOffset(p.offtable[p.trailer.TopObject])
|
||||
return
|
||||
}
|
||||
|
||||
func (p *bplistParser) readSizedInt(nbytes int) uint64 {
|
||||
switch nbytes {
|
||||
case 1:
|
||||
var val uint8
|
||||
binary.Read(p.reader, binary.BigEndian, &val)
|
||||
return uint64(val)
|
||||
case 2:
|
||||
var val uint16
|
||||
binary.Read(p.reader, binary.BigEndian, &val)
|
||||
return uint64(val)
|
||||
case 4:
|
||||
var val uint32
|
||||
binary.Read(p.reader, binary.BigEndian, &val)
|
||||
return uint64(val)
|
||||
case 8:
|
||||
var val uint64
|
||||
binary.Read(p.reader, binary.BigEndian, &val)
|
||||
return uint64(val)
|
||||
case 16:
|
||||
var high, low uint64
|
||||
binary.Read(p.reader, binary.BigEndian, &high)
|
||||
binary.Read(p.reader, binary.BigEndian, &low)
|
||||
// TODO: int128 support (!)
|
||||
return uint64(low)
|
||||
}
|
||||
panic(errors.New("illegal integer size"))
|
||||
}
|
||||
|
||||
func (p *bplistParser) countForTag(tag uint8) uint64 {
|
||||
cnt := uint64(tag & 0x0F)
|
||||
if cnt == 0xF {
|
||||
var intTag uint8
|
||||
binary.Read(p.reader, binary.BigEndian, &intTag)
|
||||
cnt = p.readSizedInt(1 << (intTag & 0xF))
|
||||
}
|
||||
return cnt
|
||||
}
|
||||
|
||||
func (p *bplistParser) valueAtOffset(off uint64) *plistValue {
|
||||
if pval, ok := p.objrefs[off]; ok {
|
||||
return pval
|
||||
}
|
||||
pval := p.parseTagAtOffset(int64(off))
|
||||
p.objrefs[off] = pval
|
||||
return pval
|
||||
}
|
||||
|
||||
func (p *bplistParser) parseTagAtOffset(off int64) *plistValue {
|
||||
var tag uint8
|
||||
p.reader.Seek(off, 0)
|
||||
binary.Read(p.reader, binary.BigEndian, &tag)
|
||||
|
||||
switch tag & 0xF0 {
|
||||
case bpTagNull:
|
||||
switch tag & 0x0F {
|
||||
case bpTagBoolTrue, bpTagBoolFalse:
|
||||
return &plistValue{Boolean, tag == bpTagBoolTrue}
|
||||
}
|
||||
return nil
|
||||
case bpTagInteger:
|
||||
val := p.readSizedInt(1 << (tag & 0xF))
|
||||
return &plistValue{Integer, signedInt{val, false}}
|
||||
case bpTagReal:
|
||||
nbytes := 1 << (tag & 0x0F)
|
||||
switch nbytes {
|
||||
case 4:
|
||||
var val float32
|
||||
binary.Read(p.reader, binary.BigEndian, &val)
|
||||
return &plistValue{Real, sizedFloat{float64(val), 32}}
|
||||
case 8:
|
||||
var val float64
|
||||
binary.Read(p.reader, binary.BigEndian, &val)
|
||||
return &plistValue{Real, sizedFloat{float64(val), 64}}
|
||||
}
|
||||
panic(errors.New("illegal float size"))
|
||||
case bpTagDate:
|
||||
var val float64
|
||||
binary.Read(p.reader, binary.BigEndian, &val)
|
||||
|
||||
// Apple Epoch is 20110101000000Z
|
||||
// Adjust for UNIX Time
|
||||
val += 978307200
|
||||
|
||||
sec, fsec := math.Modf(val)
|
||||
time := time.Unix(int64(sec), int64(fsec*float64(time.Second))).In(time.UTC)
|
||||
return &plistValue{Date, time}
|
||||
case bpTagData:
|
||||
cnt := p.countForTag(tag)
|
||||
|
||||
bytes := make([]byte, cnt)
|
||||
binary.Read(p.reader, binary.BigEndian, bytes)
|
||||
return &plistValue{Data, bytes}
|
||||
case bpTagASCIIString, bpTagUTF16String:
|
||||
cnt := p.countForTag(tag)
|
||||
|
||||
if tag&0xF0 == bpTagASCIIString {
|
||||
bytes := make([]byte, cnt)
|
||||
binary.Read(p.reader, binary.BigEndian, bytes)
|
||||
return &plistValue{String, string(bytes)}
|
||||
} else {
|
||||
bytes := make([]uint16, cnt)
|
||||
binary.Read(p.reader, binary.BigEndian, bytes)
|
||||
runes := utf16.Decode(bytes)
|
||||
return &plistValue{String, string(runes)}
|
||||
}
|
||||
case bpTagUID: // Somehow different than int: low half is nbytes - 1 instead of log2(nbytes)
|
||||
val := p.readSizedInt(int(tag&0xF) + 1)
|
||||
return &plistValue{Integer, signedInt{val, false}}
|
||||
case bpTagDictionary:
|
||||
cnt := p.countForTag(tag)
|
||||
|
||||
subvalues := make(map[string]*plistValue)
|
||||
indices := make([]uint64, cnt*2)
|
||||
for i := uint64(0); i < cnt*2; i++ {
|
||||
idx := p.readSizedInt(int(p.trailer.ObjectRefSize))
|
||||
indices[i] = idx
|
||||
}
|
||||
for i := uint64(0); i < cnt; i++ {
|
||||
kval := p.valueAtOffset(p.offtable[indices[i]])
|
||||
subvalues[kval.value.(string)] = p.valueAtOffset(p.offtable[indices[i+cnt]])
|
||||
}
|
||||
|
||||
return &plistValue{Dictionary, &dictionary{m: subvalues}}
|
||||
case bpTagArray:
|
||||
cnt := p.countForTag(tag)
|
||||
|
||||
arr := make([]*plistValue, cnt)
|
||||
indices := make([]uint64, cnt)
|
||||
for i := uint64(0); i < cnt; i++ {
|
||||
indices[i] = p.readSizedInt(int(p.trailer.ObjectRefSize))
|
||||
}
|
||||
for i := uint64(0); i < cnt; i++ {
|
||||
arr[i] = p.valueAtOffset(p.offtable[indices[i]])
|
||||
}
|
||||
|
||||
return &plistValue{Array, arr}
|
||||
}
|
||||
panic(fmt.Errorf("unexpected atom 0x%2.02x at offset %d", tag, off))
|
||||
}
|
||||
|
||||
func newBplistParser(r io.ReadSeeker) *bplistParser {
|
||||
return &bplistParser{reader: r}
|
||||
}
|
118
vendor/github.com/DHowett/go-plist/decode.go
generated
vendored
|
@ -1,118 +0,0 @@
|
|||
package plist
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"reflect"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
type parser interface {
|
||||
parseDocument() (*plistValue, error)
|
||||
}
|
||||
|
||||
// A Decoder reads a property list from an input stream.
|
||||
type Decoder struct {
|
||||
// the format of the most-recently-decoded property list
|
||||
Format int
|
||||
|
||||
reader io.ReadSeeker
|
||||
lax bool
|
||||
}
|
||||
|
||||
// Decode works like Unmarshal, except it reads the decoder stream to find property list elements.
|
||||
//
|
||||
// After Decoding, the Decoder's Format field will be set to one of the plist format constants.
|
||||
func (p *Decoder) Decode(v interface{}) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if _, ok := r.(runtime.Error); ok {
|
||||
panic(r)
|
||||
}
|
||||
err = r.(error)
|
||||
}
|
||||
}()
|
||||
|
||||
header := make([]byte, 6)
|
||||
p.reader.Read(header)
|
||||
p.reader.Seek(0, 0)
|
||||
|
||||
var parser parser
|
||||
var pval *plistValue
|
||||
if bytes.Equal(header, []byte("bplist")) {
|
||||
parser = newBplistParser(p.reader)
|
||||
pval, err = parser.parseDocument()
|
||||
if err != nil {
|
||||
// Had a bplist header, but still got an error: we have to die here.
|
||||
return err
|
||||
}
|
||||
p.Format = BinaryFormat
|
||||
} else {
|
||||
parser = newXMLPlistParser(p.reader)
|
||||
pval, err = parser.parseDocument()
|
||||
if _, ok := err.(invalidPlistError); ok {
|
||||
// Rewind: the XML parser might have exhausted the file.
|
||||
p.reader.Seek(0, 0)
|
||||
// We don't use parser here because we want the textPlistParser type
|
||||
tp := newTextPlistParser(p.reader)
|
||||
pval, err = tp.parseDocument()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Format = tp.format
|
||||
if p.Format == OpenStepFormat {
|
||||
// OpenStep property lists can only store strings,
|
||||
// so we have to turn on lax mode here for the unmarshal step later.
|
||||
p.lax = true
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Format = XMLFormat
|
||||
}
|
||||
}
|
||||
|
||||
p.unmarshal(pval, reflect.ValueOf(v))
|
||||
return
|
||||
}
|
||||
|
||||
// NewDecoder returns a Decoder that reads property list elements from a stream reader, r.
|
||||
// NewDecoder requires a Seekable stream for the purposes of file type detection.
|
||||
func NewDecoder(r io.ReadSeeker) *Decoder {
|
||||
return &Decoder{Format: InvalidFormat, reader: r, lax: false}
|
||||
}
|
||||
|
||||
// Unmarshal parses a property list document and stores the result in the value pointed to by v.
|
||||
//
|
||||
// Unmarshal uses the inverse of the type encodings that Marshal uses, allocating heap-borne types as necessary.
|
||||
//
|
||||
// When given a nil pointer, Unmarshal allocates a new value for it to point to.
|
||||
//
|
||||
// To decode property list values into an interface value, Unmarshal decodes the property list into the concrete value contained
|
||||
// in the interface value. If the interface value is nil, Unmarshal stores one of the following in the interface value:
|
||||
//
|
||||
// string, bool, uint64, float64
|
||||
// []byte, for plist data
|
||||
// []interface{}, for plist arrays
|
||||
// map[string]interface{}, for plist dictionaries
|
||||
//
|
||||
// If a property list value is not appropriate for a given value type, Unmarshal aborts immediately and returns an error.
|
||||
//
|
||||
// As Go does not support 128-bit types, and we don't want to pretend we're giving the user integer types (as opposed to
|
||||
// secretly passing them structs), Unmarshal will drop the high 64 bits of any 128-bit integers encoded in binary property lists.
|
||||
// (This is important because CoreFoundation serializes some large 64-bit values as 128-bit values with an empty high half.)
|
||||
//
|
||||
// When Unmarshal encounters an OpenStep property list, it will enter a relaxed parsing mode: OpenStep property lists can only store
|
||||
// plain old data as strings, so we will attempt to recover integer, floating-point, boolean and date values wherever they are necessary.
|
||||
// (for example, if Unmarshal attempts to unmarshal an OpenStep property list into a time.Time, it will try to parse the string it
|
||||
// receives as a time.)
|
||||
//
|
||||
// Unmarshal returns the detected property list format and an error, if any.
|
||||
func Unmarshal(data []byte, v interface{}) (format int, err error) {
|
||||
r := bytes.NewReader(data)
|
||||
dec := NewDecoder(r)
|
||||
err = dec.Decode(v)
|
||||
format = dec.Format
|
||||
return
|
||||
}
|
5
vendor/github.com/DHowett/go-plist/doc.go
generated
vendored
|
@ -1,5 +0,0 @@
|
|||
// Package plist implements encoding and decoding of Apple's "property list" format.
|
||||
// Property lists come in three sorts: plain text (GNUStep and OpenStep), XML and binary.
|
||||
// plist supports all of them.
|
||||
// The mapping between property list and Go objects is described in the documentation for the Marshal and Unmarshal functions.
|
||||
package plist
|
126
vendor/github.com/DHowett/go-plist/encode.go
generated
vendored
|
@ -1,126 +0,0 @@
|
|||
package plist
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"reflect"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
type generator interface {
|
||||
generateDocument(*plistValue)
|
||||
Indent(string)
|
||||
}
|
||||
|
||||
// An Encoder writes a property list to an output stream.
|
||||
type Encoder struct {
|
||||
writer io.Writer
|
||||
format int
|
||||
|
||||
indent string
|
||||
}
|
||||
|
||||
// Encode writes the property list encoding of v to the stream.
|
||||
func (p *Encoder) Encode(v interface{}) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if _, ok := r.(runtime.Error); ok {
|
||||
panic(r)
|
||||
}
|
||||
err = r.(error)
|
||||
}
|
||||
}()
|
||||
|
||||
pval := p.marshal(reflect.ValueOf(v))
|
||||
if pval == nil {
|
||||
panic(errors.New("plist: no root element to encode"))
|
||||
}
|
||||
|
||||
var g generator
|
||||
switch p.format {
|
||||
case XMLFormat:
|
||||
g = newXMLPlistGenerator(p.writer)
|
||||
case BinaryFormat, AutomaticFormat:
|
||||
g = newBplistGenerator(p.writer)
|
||||
case OpenStepFormat, GNUStepFormat:
|
||||
g = newTextPlistGenerator(p.writer, p.format)
|
||||
}
|
||||
g.Indent(p.indent)
|
||||
g.generateDocument(pval)
|
||||
return
|
||||
}
|
||||
|
||||
// Indent turns on pretty-printing for the XML and Text property list formats.
|
||||
// Each element begins on a new line and is preceded by one or more copies of indent according to its nesting depth.
|
||||
func (p *Encoder) Indent(indent string) {
|
||||
p.indent = indent
|
||||
}
|
||||
|
||||
// NewEncoder returns an Encoder that writes an XML property list to w.
|
||||
func NewEncoder(w io.Writer) *Encoder {
|
||||
return NewEncoderForFormat(w, XMLFormat)
|
||||
}
|
||||
|
||||
// NewEncoderForFormat returns an Encoder that writes a property list to w in the specified format.
|
||||
// Pass AutomaticFormat to allow the library to choose the best encoding (currently BinaryFormat).
|
||||
func NewEncoderForFormat(w io.Writer, format int) *Encoder {
|
||||
return &Encoder{
|
||||
writer: w,
|
||||
format: format,
|
||||
}
|
||||
}
|
||||
|
||||
// NewBinaryEncoder returns an Encoder that writes a binary property list to w.
|
||||
func NewBinaryEncoder(w io.Writer) *Encoder {
|
||||
return NewEncoderForFormat(w, BinaryFormat)
|
||||
}
|
||||
|
||||
// Marshal returns the property list encoding of v in the specified format.
|
||||
//
|
||||
// Pass AutomaticFormat to allow the library to choose the best encoding (currently BinaryFormat).
|
||||
//
|
||||
// Marshal traverses the value v recursively.
|
||||
// Any nil values encountered, other than the root, will be silently discarded as
|
||||
// the property list format bears no representation for nil values.
|
||||
//
|
||||
// Strings, integers of varying size, floats and booleans are encoded unchanged.
|
||||
// Strings bearing non-ASCII runes will be encoded differently depending upon the property list format:
|
||||
// UTF-8 for XML property lists and UTF-16 for binary property lists.
|
||||
//
|
||||
// Slice and Array values are encoded as property list arrays, except for
|
||||
// []byte values, which are encoded as data.
|
||||
//
|
||||
// Map values encode as dictionaries. The map's key type must be string; there is no provision for encoding non-string dictionary keys.
|
||||
//
|
||||
// Struct values are encoded as dictionaries, with only exported fields being serialized. Struct field encoding may be influenced with the use of tags.
|
||||
// The tag format is:
|
||||
//
|
||||
// `plist:"<key>[,flags...]"`
|
||||
//
|
||||
// The following flags are supported:
|
||||
//
|
||||
// omitempty Only include the field if it is not set to the zero value for its type.
|
||||
//
|
||||
// If the key is "-", the field is ignored.
|
||||
//
|
||||
// Anonymous struct fields are encoded as if their exported fields were exposed via the outer struct.
|
||||
//
|
||||
// Pointer values encode as the value pointed to.
|
||||
//
|
||||
// Channel, complex and function values cannot be encoded. Any attempt to do so causes Marshal to return an error.
|
||||
func Marshal(v interface{}, format int) ([]byte, error) {
|
||||
return MarshalIndent(v, format, "")
|
||||
}
|
||||
|
||||
// MarshalIndent works like Marshal, but each property list element
|
||||
// begins on a new line and is preceded by one or more copies of indent according to its nesting depth.
|
||||
func MarshalIndent(v interface{}, format int, indent string) ([]byte, error) {
|
||||
buf := &bytes.Buffer{}
|
||||
enc := NewEncoderForFormat(buf, format)
|
||||
enc.Indent(indent)
|
||||
if err := enc.Encode(v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
154
vendor/github.com/DHowett/go-plist/marshal.go
generated
vendored
|
@ -1,154 +0,0 @@
|
|||
package plist
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
func isEmptyValue(v reflect.Value) bool {
|
||||
switch v.Kind() {
|
||||
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
|
||||
return v.Len() == 0
|
||||
case reflect.Bool:
|
||||
return !v.Bool()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return v.Int() == 0
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
return v.Uint() == 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return v.Float() == 0
|
||||
case reflect.Interface, reflect.Ptr:
|
||||
return v.IsNil()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var (
|
||||
textMarshalerType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()
|
||||
timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
|
||||
)
|
||||
|
||||
func (p *Encoder) marshalTextInterface(marshalable encoding.TextMarshaler) *plistValue {
|
||||
s, err := marshalable.MarshalText()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &plistValue{String, string(s)}
|
||||
}
|
||||
|
||||
func (p *Encoder) marshalStruct(typ reflect.Type, val reflect.Value) *plistValue {
|
||||
tinfo, _ := getTypeInfo(typ)
|
||||
|
||||
dict := &dictionary{
|
||||
m: make(map[string]*plistValue, len(tinfo.fields)),
|
||||
}
|
||||
for _, finfo := range tinfo.fields {
|
||||
value := finfo.value(val)
|
||||
if !value.IsValid() || finfo.omitEmpty && isEmptyValue(value) {
|
||||
continue
|
||||
}
|
||||
dict.m[finfo.name] = p.marshal(value)
|
||||
}
|
||||
|
||||
return &plistValue{Dictionary, dict}
|
||||
}
|
||||
|
||||
func (p *Encoder) marshalTime(val reflect.Value) *plistValue {
|
||||
time := val.Interface().(time.Time)
|
||||
return &plistValue{Date, time}
|
||||
}
|
||||
|
||||
func (p *Encoder) marshal(val reflect.Value) *plistValue {
|
||||
if !val.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// time.Time implements TextMarshaler, but we need to store it in RFC3339
|
||||
if val.Type() == timeType {
|
||||
return p.marshalTime(val)
|
||||
}
|
||||
if val.Kind() == reflect.Ptr || (val.Kind() == reflect.Interface && val.NumMethod() == 0) {
|
||||
ival := val.Elem()
|
||||
if ival.IsValid() && ival.Type() == timeType {
|
||||
return p.marshalTime(ival)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for text marshaler.
|
||||
if val.CanInterface() && val.Type().Implements(textMarshalerType) {
|
||||
return p.marshalTextInterface(val.Interface().(encoding.TextMarshaler))
|
||||
}
|
||||
if val.CanAddr() {
|
||||
pv := val.Addr()
|
||||
if pv.CanInterface() && pv.Type().Implements(textMarshalerType) {
|
||||
return p.marshalTextInterface(pv.Interface().(encoding.TextMarshaler))
|
||||
}
|
||||
}
|
||||
|
||||
// Descend into pointers or interfaces
|
||||
if val.Kind() == reflect.Ptr || (val.Kind() == reflect.Interface && val.NumMethod() == 0) {
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
// We got this far and still may have an invalid anything or nil ptr/interface
|
||||
if !val.IsValid() || ((val.Kind() == reflect.Ptr || val.Kind() == reflect.Interface) && val.IsNil()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
typ := val.Type()
|
||||
|
||||
if val.Kind() == reflect.Struct {
|
||||
return p.marshalStruct(typ, val)
|
||||
}
|
||||
|
||||
switch val.Kind() {
|
||||
case reflect.String:
|
||||
return &plistValue{String, val.String()}
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return &plistValue{Integer, signedInt{uint64(val.Int()), true}}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
return &plistValue{Integer, signedInt{uint64(val.Uint()), false}}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return &plistValue{Real, sizedFloat{val.Float(), val.Type().Bits()}}
|
||||
case reflect.Bool:
|
||||
return &plistValue{Boolean, val.Bool()}
|
||||
case reflect.Slice, reflect.Array:
|
||||
if typ.Elem().Kind() == reflect.Uint8 {
|
||||
bytes := []byte(nil)
|
||||
if val.CanAddr() {
|
||||
bytes = val.Bytes()
|
||||
} else {
|
||||
bytes = make([]byte, val.Len())
|
||||
reflect.Copy(reflect.ValueOf(bytes), val)
|
||||
}
|
||||
return &plistValue{Data, bytes}
|
||||
} else {
|
||||
subvalues := make([]*plistValue, val.Len())
|
||||
for idx, length := 0, val.Len(); idx < length; idx++ {
|
||||
if subpval := p.marshal(val.Index(idx)); subpval != nil {
|
||||
subvalues[idx] = subpval
|
||||
}
|
||||
}
|
||||
return &plistValue{Array, subvalues}
|
||||
}
|
||||
case reflect.Map:
|
||||
if typ.Key().Kind() != reflect.String {
|
||||
panic(&unknownTypeError{typ})
|
||||
}
|
||||
|
||||
l := val.Len()
|
||||
dict := &dictionary{
|
||||
m: make(map[string]*plistValue, l),
|
||||
}
|
||||
for _, keyv := range val.MapKeys() {
|
||||
if subpval := p.marshal(val.MapIndex(keyv)); subpval != nil {
|
||||
dict.m[keyv.String()] = subpval
|
||||
}
|
||||
}
|
||||
return &plistValue{Dictionary, dict}
|
||||
default:
|
||||
panic(&unknownTypeError{typ})
|
||||
}
|
||||
return nil
|
||||
}
|
50
vendor/github.com/DHowett/go-plist/must.go
generated
vendored
|
@ -1,50 +0,0 @@
|
|||
package plist
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type mustWriter struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (w mustWriter) Write(p []byte) (int, error) {
|
||||
n, err := w.Writer.Write(p)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func mustParseInt(str string, base, bits int) int64 {
|
||||
i, err := strconv.ParseInt(str, base, bits)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func mustParseUint(str string, base, bits int) uint64 {
|
||||
i, err := strconv.ParseUint(str, base, bits)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func mustParseFloat(str string, bits int) float64 {
|
||||
i, err := strconv.ParseFloat(str, bits)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func mustParseBool(str string) bool {
|
||||
i, err := strconv.ParseBool(str)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return i
|
||||
}
|
141
vendor/github.com/DHowett/go-plist/plist.go
generated
vendored
|
@ -1,141 +0,0 @@
|
|||
package plist
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Property list format constants
|
||||
const (
|
||||
// Used by Decoder to represent an invalid property list.
|
||||
InvalidFormat int = 0
|
||||
|
||||
// Used to indicate total abandon with regards to Encoder's output format.
|
||||
AutomaticFormat = 0
|
||||
|
||||
XMLFormat = 1
|
||||
BinaryFormat = 2
|
||||
OpenStepFormat = 3
|
||||
GNUStepFormat = 4
|
||||
)
|
||||
|
||||
var FormatNames = map[int]string{
|
||||
InvalidFormat: "unknown/invalid",
|
||||
XMLFormat: "XML",
|
||||
BinaryFormat: "Binary",
|
||||
OpenStepFormat: "OpenStep",
|
||||
GNUStepFormat: "GNUStep",
|
||||
}
|
||||
|
||||
type plistKind uint
|
||||
|
||||
const (
|
||||
Invalid plistKind = iota
|
||||
Dictionary
|
||||
Array
|
||||
String
|
||||
Integer
|
||||
Real
|
||||
Boolean
|
||||
Data
|
||||
Date
|
||||
)
|
||||
|
||||
var plistKindNames map[plistKind]string = map[plistKind]string{
|
||||
Invalid: "invalid",
|
||||
Dictionary: "dictionary",
|
||||
Array: "array",
|
||||
String: "string",
|
||||
Integer: "integer",
|
||||
Real: "real",
|
||||
Boolean: "boolean",
|
||||
Data: "data",
|
||||
Date: "date",
|
||||
}
|
||||
|
||||
type plistValue struct {
|
||||
kind plistKind
|
||||
value interface{}
|
||||
}
|
||||
|
||||
type signedInt struct {
|
||||
value uint64
|
||||
signed bool
|
||||
}
|
||||
|
||||
type sizedFloat struct {
|
||||
value float64
|
||||
bits int
|
||||
}
|
||||
|
||||
type dictionary struct {
|
||||
count int
|
||||
m map[string]*plistValue
|
||||
keys sort.StringSlice
|
||||
values []*plistValue
|
||||
}
|
||||
|
||||
func (d *dictionary) Len() int {
|
||||
return d.count
|
||||
}
|
||||
|
||||
func (d *dictionary) Less(i, j int) bool {
|
||||
return d.keys.Less(i, j)
|
||||
}
|
||||
|
||||
func (d *dictionary) Swap(i, j int) {
|
||||
d.keys.Swap(i, j)
|
||||
d.values[i], d.values[j] = d.values[j], d.values[i]
|
||||
}
|
||||
|
||||
func (d *dictionary) populateArrays() {
|
||||
if d.count > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
l := len(d.m)
|
||||
d.count = l
|
||||
d.keys = make([]string, l)
|
||||
d.values = make([]*plistValue, l)
|
||||
i := 0
|
||||
for k, v := range d.m {
|
||||
d.keys[i] = k
|
||||
d.values[i] = v
|
||||
i++
|
||||
}
|
||||
sort.Sort(d)
|
||||
}
|
||||
|
||||
type unknownTypeError struct {
|
||||
typ reflect.Type
|
||||
}
|
||||
|
||||
func (u *unknownTypeError) Error() string {
|
||||
return "plist: can't marshal value of type " + u.typ.String()
|
||||
}
|
||||
|
||||
type invalidPlistError struct {
|
||||
format string
|
||||
err error
|
||||
}
|
||||
|
||||
func (e invalidPlistError) Error() string {
|
||||
s := "plist: invalid " + e.format + " property list"
|
||||
if e.err != nil {
|
||||
s += ": " + e.err.Error()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type plistParseError struct {
|
||||
format string
|
||||
err error
|
||||
}
|
||||
|
||||
func (e plistParseError) Error() string {
|
||||
s := "plist: error parsing " + e.format + " property list"
|
||||
if e.err != nil {
|
||||
s += ": " + e.err.Error()
|
||||
}
|
||||
return s
|
||||
}
|
565
vendor/github.com/DHowett/go-plist/text.go
generated
vendored
|
@ -1,565 +0,0 @@
|
|||
package plist
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type textPlistGenerator struct {
|
||||
writer io.Writer
|
||||
format int
|
||||
|
||||
quotableTable *[4]uint64
|
||||
|
||||
indent string
|
||||
depth int
|
||||
|
||||
dictKvDelimiter, dictEntryDelimiter, arrayDelimiter []byte
|
||||
}
|
||||
|
||||
var (
|
||||
textPlistTimeLayout = "2006-01-02 15:04:05 -0700"
|
||||
padding = "0000"
|
||||
)
|
||||
|
||||
func (p *textPlistGenerator) generateDocument(pval *plistValue) {
|
||||
p.writePlistValue(pval)
|
||||
}
|
||||
|
||||
func (p *textPlistGenerator) plistQuotedString(str string) string {
|
||||
if str == "" {
|
||||
return `""`
|
||||
}
|
||||
s := ""
|
||||
quot := false
|
||||
for _, r := range str {
|
||||
if r > 0xFF {
|
||||
quot = true
|
||||
s += `\U`
|
||||
us := strconv.FormatInt(int64(r), 16)
|
||||
s += padding[len(us):]
|
||||
s += us
|
||||
} else if r > 0x7F {
|
||||
quot = true
|
||||
s += `\`
|
||||
us := strconv.FormatInt(int64(r), 8)
|
||||
s += padding[1+len(us):]
|
||||
s += us
|
||||
} else {
|
||||
c := uint8(r)
|
||||
if (*p.quotableTable)[c/64]&(1<<(c%64)) > 0 {
|
||||
quot = true
|
||||
}
|
||||
|
||||
switch c {
|
||||
case '\a':
|
||||
s += `\a`
|
||||
case '\b':
|
||||
s += `\b`
|
||||
case '\v':
|
||||
s += `\v`
|
||||
case '\f':
|
||||
s += `\f`
|
||||
case '\\':
|
||||
s += `\\`
|
||||
case '"':
|
||||
s += `\"`
|
||||
case '\t', '\r', '\n':
|
||||
fallthrough
|
||||
default:
|
||||
s += string(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
if quot {
|
||||
s = `"` + s + `"`
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (p *textPlistGenerator) deltaIndent(depthDelta int) {
|
||||
if depthDelta < 0 {
|
||||
p.depth--
|
||||
} else if depthDelta > 0 {
|
||||
p.depth++
|
||||
}
|
||||
}
|
||||
|
||||
func (p *textPlistGenerator) writeIndent() {
|
||||
if len(p.indent) == 0 {
|
||||
return
|
||||
}
|
||||
if len(p.indent) > 0 {
|
||||
p.writer.Write([]byte("\n"))
|
||||
for i := 0; i < p.depth; i++ {
|
||||
io.WriteString(p.writer, p.indent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *textPlistGenerator) writePlistValue(pval *plistValue) {
|
||||
if pval == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch pval.kind {
|
||||
case Dictionary:
|
||||
p.writer.Write([]byte(`{`))
|
||||
p.deltaIndent(1)
|
||||
dict := pval.value.(*dictionary)
|
||||
dict.populateArrays()
|
||||
for i, k := range dict.keys {
|
||||
p.writeIndent()
|
||||
io.WriteString(p.writer, p.plistQuotedString(k))
|
||||
p.writer.Write(p.dictKvDelimiter)
|
||||
p.writePlistValue(dict.values[i])
|
||||
p.writer.Write(p.dictEntryDelimiter)
|
||||
}
|
||||
p.deltaIndent(-1)
|
||||
p.writeIndent()
|
||||
p.writer.Write([]byte(`}`))
|
||||
case Array:
|
||||
p.writer.Write([]byte(`(`))
|
||||
p.deltaIndent(1)
|
||||
values := pval.value.([]*plistValue)
|
||||
for _, v := range values {
|
||||
p.writeIndent()
|
||||
p.writePlistValue(v)
|
||||
p.writer.Write(p.arrayDelimiter)
|
||||
}
|
||||
p.deltaIndent(-1)
|
||||
p.writeIndent()
|
||||
p.writer.Write([]byte(`)`))
|
||||
case String:
|
||||
io.WriteString(p.writer, p.plistQuotedString(pval.value.(string)))
|
||||
case Integer:
|
||||
if p.format == GNUStepFormat {
|
||||
p.writer.Write([]byte(`<*I`))
|
||||
}
|
||||
if pval.value.(signedInt).signed {
|
||||
io.WriteString(p.writer, strconv.FormatInt(int64(pval.value.(signedInt).value), 10))
|
||||
} else {
|
||||
io.WriteString(p.writer, strconv.FormatUint(pval.value.(signedInt).value, 10))
|
||||
}
|
||||
if p.format == GNUStepFormat {
|
||||
p.writer.Write([]byte(`>`))
|
||||
}
|
||||
case Real:
|
||||
if p.format == GNUStepFormat {
|
||||
p.writer.Write([]byte(`<*R`))
|
||||
}
|
||||
io.WriteString(p.writer, strconv.FormatFloat(pval.value.(sizedFloat).value, 'g', -1, 64))
|
||||
if p.format == GNUStepFormat {
|
||||
p.writer.Write([]byte(`>`))
|
||||
}
|
||||
case Boolean:
|
||||
b := pval.value.(bool)
|
||||
if p.format == GNUStepFormat {
|
||||
if b {
|
||||
p.writer.Write([]byte(`<*BY>`))
|
||||
} else {
|
||||
p.writer.Write([]byte(`<*BN>`))
|
||||
}
|
||||
} else {
|
||||
if b {
|
||||
p.writer.Write([]byte(`1`))
|
||||
} else {
|
||||
p.writer.Write([]byte(`0`))
|
||||
}
|
||||
}
|
||||
case Data:
|
||||
b := pval.value.([]byte)
|
||||
var hexencoded [9]byte
|
||||
var l int
|
||||
var asc = 9
|
||||
hexencoded[8] = ' '
|
||||
|
||||
p.writer.Write([]byte(`<`))
|
||||
for i := 0; i < len(b); i += 4 {
|
||||
l = i + 4
|
||||
if l >= len(b) {
|
||||
l = len(b)
|
||||
// We no longer need the space - or the rest of the buffer.
|
||||
// (we used >= above to get this part without another conditional :P)
|
||||
asc = (l - i) * 2
|
||||
}
|
||||
// Fill the buffer (only up to 8 characters, to preserve the space we implicitly include
|
||||
// at the end of every encode)
|
||||
hex.Encode(hexencoded[:8], b[i:l])
|
||||
io.WriteString(p.writer, string(hexencoded[:asc]))
|
||||
}
|
||||
p.writer.Write([]byte(`>`))
|
||||
case Date:
|
||||
if p.format == GNUStepFormat {
|
||||
p.writer.Write([]byte(`<*D`))
|
||||
io.WriteString(p.writer, pval.value.(time.Time).In(time.UTC).Format(textPlistTimeLayout))
|
||||
p.writer.Write([]byte(`>`))
|
||||
} else {
|
||||
io.WriteString(p.writer, p.plistQuotedString(pval.value.(time.Time).In(time.UTC).Format(textPlistTimeLayout)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *textPlistGenerator) Indent(i string) {
|
||||
p.indent = i
|
||||
if i == "" {
|
||||
p.dictKvDelimiter = []byte(`=`)
|
||||
} else {
|
||||
// For pretty-printing
|
||||
p.dictKvDelimiter = []byte(` = `)
|
||||
}
|
||||
}
|
||||
|
||||
func newTextPlistGenerator(w io.Writer, format int) *textPlistGenerator {
|
||||
table := &osQuotable
|
||||
if format == GNUStepFormat {
|
||||
table = &gsQuotable
|
||||
}
|
||||
return &textPlistGenerator{
|
||||
writer: mustWriter{w},
|
||||
format: format,
|
||||
quotableTable: table,
|
||||
dictKvDelimiter: []byte(`=`),
|
||||
arrayDelimiter: []byte(`,`),
|
||||
dictEntryDelimiter: []byte(`;`),
|
||||
}
|
||||
}
|
||||
|
||||
type byteReader interface {
|
||||
io.Reader
|
||||
io.ByteScanner
|
||||
Peek(n int) ([]byte, error)
|
||||
ReadBytes(delim byte) ([]byte, error)
|
||||
}
|
||||
|
||||
type textPlistParser struct {
|
||||
reader byteReader
|
||||
whitespaceReplacer *strings.Replacer
|
||||
format int
|
||||
}
|
||||
|
||||
func (p *textPlistParser) parseDocument() (pval *plistValue, parseError error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if _, ok := r.(runtime.Error); ok {
|
||||
panic(r)
|
||||
}
|
||||
if _, ok := r.(invalidPlistError); ok {
|
||||
parseError = r.(error)
|
||||
} else {
|
||||
// Wrap all non-invalid-plist errors.
|
||||
parseError = plistParseError{"text", r.(error)}
|
||||
}
|
||||
}
|
||||
}()
|
||||
pval = p.parsePlistValue()
|
||||
return
|
||||
}
|
||||
|
||||
func (p *textPlistParser) chugWhitespace() {
|
||||
ws:
|
||||
for {
|
||||
c, err := p.reader.ReadByte()
|
||||
if err != nil && err != io.EOF {
|
||||
panic(err)
|
||||
}
|
||||
if whitespace[c/64]&(1<<(c%64)) == 0 {
|
||||
if c == '/' && err != io.EOF {
|
||||
// A / at the end of the file is not the begining of a comment.
|
||||
cs, err := p.reader.Peek(1)
|
||||
if err != nil && err != io.EOF {
|
||||
panic(err)
|
||||
}
|
||||
c = cs[0]
|
||||
switch c {
|
||||
case '/':
|
||||
for {
|
||||
c, err = p.reader.ReadByte()
|
||||
if err != nil && err != io.EOF {
|
||||
panic(err)
|
||||
} else if err == io.EOF {
|
||||
break
|
||||
}
|
||||
// TODO: UTF-8
|
||||
if c == '\n' || c == '\r' {
|
||||
break
|
||||
}
|
||||
}
|
||||
case '*':
|
||||
// Peek returned a value here, so it is safe to read.
|
||||
_, _ = p.reader.ReadByte()
|
||||
star := false
|
||||
for {
|
||||
c, err = p.reader.ReadByte()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if c == '*' {
|
||||
star = true
|
||||
} else if c == '/' && star {
|
||||
break
|
||||
} else {
|
||||
star = false
|
||||
}
|
||||
}
|
||||
default:
|
||||
p.reader.UnreadByte() // Not the beginning of a // or /* comment
|
||||
break ws
|
||||
}
|
||||
continue
|
||||
}
|
||||
p.reader.UnreadByte()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *textPlistParser) parseQuotedString() *plistValue {
|
||||
escaping := false
|
||||
s := ""
|
||||
for {
|
||||
byt, err := p.reader.ReadByte()
|
||||
// EOF here is an error: we're inside a quoted string!
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c := rune(byt)
|
||||
if !escaping {
|
||||
if c == '"' {
|
||||
break
|
||||
} else if c == '\\' {
|
||||
escaping = true
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
escaping = false
|
||||
// Everything that is not listed here passes through unharmed.
|
||||
switch c {
|
||||
case 'a':
|
||||
c = '\a'
|
||||
case 'b':
|
||||
c = '\b'
|
||||
case 'v':
|
||||
c = '\v'
|
||||
case 'f':
|
||||
c = '\f'
|
||||
case 't':
|
||||
c = '\t'
|
||||
case 'r':
|
||||
c = '\r'
|
||||
case 'n':
|
||||
c = '\n'
|
||||
case 'x', 'u', 'U': // hex and unicode
|
||||
l := 4
|
||||
if c == 'x' {
|
||||
l = 2
|
||||
}
|
||||
hex := make([]byte, l)
|
||||
p.reader.Read(hex)
|
||||
newc := mustParseInt(string(hex), 16, 16)
|
||||
c = rune(newc)
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7': // octal!
|
||||
oct := make([]byte, 3)
|
||||
oct[0] = uint8(c)
|
||||
p.reader.Read(oct[1:])
|
||||
newc := mustParseInt(string(oct), 8, 16)
|
||||
c = rune(newc)
|
||||
}
|
||||
}
|
||||
s += string(c)
|
||||
}
|
||||
return &plistValue{String, s}
|
||||
}
|
||||
|
||||
func (p *textPlistParser) parseUnquotedString() *plistValue {
|
||||
s := ""
|
||||
for {
|
||||
c, err := p.reader.ReadByte()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
// if we encounter a character that must be quoted, we're done.
|
||||
// the GNUStep quote table is more lax here, so we use it instead of the OpenStep one.
|
||||
if gsQuotable[c/64]&(1<<(c%64)) > 0 {
|
||||
p.reader.UnreadByte()
|
||||
break
|
||||
}
|
||||
s += string(c)
|
||||
}
|
||||
return &plistValue{String, s}
|
||||
}
|
||||
|
||||
func (p *textPlistParser) parseDictionary() *plistValue {
|
||||
var keypv *plistValue
|
||||
subval := make(map[string]*plistValue)
|
||||
for {
|
||||
p.chugWhitespace()
|
||||
|
||||
c, err := p.reader.ReadByte()
|
||||
// EOF here is an error: we're inside a dictionary!
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if c == '}' {
|
||||
break
|
||||
} else if c == '"' {
|
||||
keypv = p.parseQuotedString()
|
||||
} else {
|
||||
p.reader.UnreadByte() // Whoops, ate part of the string
|
||||
keypv = p.parseUnquotedString()
|
||||
}
|
||||
if keypv == nil {
|
||||
// TODO better error
|
||||
panic(errors.New("missing dictionary key"))
|
||||
}
|
||||
|
||||
p.chugWhitespace()
|
||||
c, err = p.reader.ReadByte()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if c != '=' {
|
||||
panic(errors.New("missing = in dictionary"))
|
||||
}
|
||||
|
||||
// whitespace is guzzled within
|
||||
val := p.parsePlistValue()
|
||||
|
||||
p.chugWhitespace()
|
||||
c, err = p.reader.ReadByte()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if c != ';' {
|
||||
panic(errors.New("missing ; in dictionary"))
|
||||
}
|
||||
|
||||
subval[keypv.value.(string)] = val
|
||||
}
|
||||
return &plistValue{Dictionary, &dictionary{m: subval}}
|
||||
}
|
||||
|
||||
func (p *textPlistParser) parseArray() *plistValue {
|
||||
subval := make([]*plistValue, 0, 10)
|
||||
for {
|
||||
c, err := p.reader.ReadByte()
|
||||
// EOF here is an error: we're inside an array!
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if c == ')' {
|
||||
break
|
||||
} else if c == ',' {
|
||||
continue
|
||||
}
|
||||
|
||||
p.reader.UnreadByte()
|
||||
pval := p.parsePlistValue()
|
||||
if pval.kind == String && pval.value.(string) == "" {
|
||||
continue
|
||||
}
|
||||
subval = append(subval, pval)
|
||||
}
|
||||
return &plistValue{Array, subval}
|
||||
}
|
||||
|
||||
func (p *textPlistParser) parseGNUStepValue(v []byte) *plistValue {
|
||||
if len(v) < 2 {
|
||||
panic(errors.New("invalid GNUStep extended value"))
|
||||
}
|
||||
typ := v[1]
|
||||
v = v[2:]
|
||||
switch typ {
|
||||
case 'I':
|
||||
if v[0] == '-' {
|
||||
n := mustParseInt(string(v), 10, 64)
|
||||
return &plistValue{Integer, signedInt{uint64(n), true}}
|
||||
} else {
|
||||
n := mustParseUint(string(v), 10, 64)
|
||||
return &plistValue{Integer, signedInt{n, false}}
|
||||
}
|
||||
case 'R':
|
||||
n := mustParseFloat(string(v), 64)
|
||||
return &plistValue{Real, sizedFloat{n, 64}}
|
||||
case 'B':
|
||||
b := v[0] == 'Y'
|
||||
return &plistValue{Boolean, b}
|
||||
case 'D':
|
||||
t, err := time.Parse(textPlistTimeLayout, string(v))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &plistValue{Date, t.In(time.UTC)}
|
||||
}
|
||||
panic(errors.New("invalid GNUStep type " + string(typ)))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *textPlistParser) parsePlistValue() *plistValue {
|
||||
for {
|
||||
p.chugWhitespace()
|
||||
|
||||
c, err := p.reader.ReadByte()
|
||||
if err != nil && err != io.EOF {
|
||||
panic(err)
|
||||
}
|
||||
switch c {
|
||||
case '<':
|
||||
bytes, err := p.reader.ReadBytes('>')
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
bytes = bytes[:len(bytes)-1]
|
||||
|
||||
if bytes[0] == '*' {
|
||||
p.format = GNUStepFormat
|
||||
return p.parseGNUStepValue(bytes)
|
||||
} else {
|
||||
s := p.whitespaceReplacer.Replace(string(bytes))
|
||||
data, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &plistValue{Data, data}
|
||||
}
|
||||
case '"':
|
||||
return p.parseQuotedString()
|
||||
case '{':
|
||||
return p.parseDictionary()
|
||||
case '(':
|
||||
return p.parseArray()
|
||||
default:
|
||||
p.reader.UnreadByte() // Place back in buffer for parseUnquotedString
|
||||
return p.parseUnquotedString()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newTextPlistParser(r io.Reader) *textPlistParser {
|
||||
var reader byteReader
|
||||
if rd, ok := r.(byteReader); ok {
|
||||
reader = rd
|
||||
} else {
|
||||
reader = bufio.NewReader(r)
|
||||
}
|
||||
return &textPlistParser{
|
||||
reader: reader,
|
||||
whitespaceReplacer: strings.NewReplacer("\t", "", "\n", "", " ", "", "\r", ""),
|
||||
format: OpenStepFormat,
|
||||
}
|
||||
}
|
26
vendor/github.com/DHowett/go-plist/text_tables.go
generated
vendored
|
@ -1,26 +0,0 @@
|
|||
package plist
|
||||
|
||||
// Bitmap of characters that must be inside a quoted string
|
||||
// when written to an old-style property list
|
||||
// Low bits represent lower characters, and each uint64 represents 64 characters.
|
||||
var gsQuotable = [4]uint64{
|
||||
0x78001385ffffffff,
|
||||
0xa800000138000000,
|
||||
0xffffffffffffffff,
|
||||
0xffffffffffffffff,
|
||||
}
|
||||
|
||||
// 7f instead of 3f in the top line: CFOldStylePlist.c says . is valid, but they quote it.
|
||||
var osQuotable = [4]uint64{
|
||||
0xf4007f6fffffffff,
|
||||
0xf8000001f8000001,
|
||||
0xffffffffffffffff,
|
||||
0xffffffffffffffff,
|
||||
}
|
||||
|
||||
var whitespace = [4]uint64{
|
||||
0x0000000100003f00,
|
||||
0x0000000000000000,
|
||||
0x0000000000000000,
|
||||
0x0000000000000000,
|
||||
}
|
170
vendor/github.com/DHowett/go-plist/typeinfo.go
generated
vendored
|
@ -1,170 +0,0 @@
|
|||
package plist
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// typeInfo holds details for the plist representation of a type.
|
||||
type typeInfo struct {
|
||||
fields []fieldInfo
|
||||
}
|
||||
|
||||
// fieldInfo holds details for the plist representation of a single field.
|
||||
type fieldInfo struct {
|
||||
idx []int
|
||||
name string
|
||||
omitEmpty bool
|
||||
}
|
||||
|
||||
var tinfoMap = make(map[reflect.Type]*typeInfo)
|
||||
var tinfoLock sync.RWMutex
|
||||
|
||||
// getTypeInfo returns the typeInfo structure with details necessary
|
||||
// for marshalling and unmarshalling typ.
|
||||
func getTypeInfo(typ reflect.Type) (*typeInfo, error) {
|
||||
tinfoLock.RLock()
|
||||
tinfo, ok := tinfoMap[typ]
|
||||
tinfoLock.RUnlock()
|
||||
if ok {
|
||||
return tinfo, nil
|
||||
}
|
||||
tinfo = &typeInfo{}
|
||||
if typ.Kind() == reflect.Struct {
|
||||
n := typ.NumField()
|
||||
for i := 0; i < n; i++ {
|
||||
f := typ.Field(i)
|
||||
if f.PkgPath != "" || f.Tag.Get("plist") == "-" {
|
||||
continue // Private field
|
||||
}
|
||||
|
||||
// For embedded structs, embed its fields.
|
||||
if f.Anonymous {
|
||||
t := f.Type
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
if t.Kind() == reflect.Struct {
|
||||
inner, err := getTypeInfo(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, finfo := range inner.fields {
|
||||
finfo.idx = append([]int{i}, finfo.idx...)
|
||||
if err := addFieldInfo(typ, tinfo, &finfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
finfo, err := structFieldInfo(typ, &f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add the field if it doesn't conflict with other fields.
|
||||
if err := addFieldInfo(typ, tinfo, finfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
tinfoLock.Lock()
|
||||
tinfoMap[typ] = tinfo
|
||||
tinfoLock.Unlock()
|
||||
return tinfo, nil
|
||||
}
|
||||
|
||||
// structFieldInfo builds and returns a fieldInfo for f.
|
||||
func structFieldInfo(typ reflect.Type, f *reflect.StructField) (*fieldInfo, error) {
|
||||
finfo := &fieldInfo{idx: f.Index}
|
||||
|
||||
// Split the tag from the xml namespace if necessary.
|
||||
tag := f.Tag.Get("plist")
|
||||
|
||||
// Parse flags.
|
||||
tokens := strings.Split(tag, ",")
|
||||
tag = tokens[0]
|
||||
if len(tokens) > 1 {
|
||||
tag = tokens[0]
|
||||
for _, flag := range tokens[1:] {
|
||||
switch flag {
|
||||
case "omitempty":
|
||||
finfo.omitEmpty = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tag == "" {
|
||||
// If the name part of the tag is completely empty,
|
||||
// use the field name
|
||||
finfo.name = f.Name
|
||||
return finfo, nil
|
||||
}
|
||||
|
||||
finfo.name = tag
|
||||
return finfo, nil
|
||||
}
|
||||
|
||||
// addFieldInfo adds finfo to tinfo.fields if there are no
|
||||
// conflicts, or if conflicts arise from previous fields that were
|
||||
// obtained from deeper embedded structures than finfo. In the latter
|
||||
// case, the conflicting entries are dropped.
|
||||
// A conflict occurs when the path (parent + name) to a field is
|
||||
// itself a prefix of another path, or when two paths match exactly.
|
||||
// It is okay for field paths to share a common, shorter prefix.
|
||||
func addFieldInfo(typ reflect.Type, tinfo *typeInfo, newf *fieldInfo) error {
|
||||
var conflicts []int
|
||||
// First, figure all conflicts. Most working code will have none.
|
||||
for i := range tinfo.fields {
|
||||
oldf := &tinfo.fields[i]
|
||||
if newf.name == oldf.name {
|
||||
conflicts = append(conflicts, i)
|
||||
}
|
||||
}
|
||||
|
||||
// Without conflicts, add the new field and return.
|
||||
if conflicts == nil {
|
||||
tinfo.fields = append(tinfo.fields, *newf)
|
||||
return nil
|
||||
}
|
||||
|
||||
// If any conflict is shallower, ignore the new field.
|
||||
// This matches the Go field resolution on embedding.
|
||||
for _, i := range conflicts {
|
||||
if len(tinfo.fields[i].idx) < len(newf.idx) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, the new field is shallower, and thus takes precedence,
|
||||
// so drop the conflicting fields from tinfo and append the new one.
|
||||
for c := len(conflicts) - 1; c >= 0; c-- {
|
||||
i := conflicts[c]
|
||||
copy(tinfo.fields[i:], tinfo.fields[i+1:])
|
||||
tinfo.fields = tinfo.fields[:len(tinfo.fields)-1]
|
||||
}
|
||||
tinfo.fields = append(tinfo.fields, *newf)
|
||||
return nil
|
||||
}
|
||||
|
||||
// value returns v's field value corresponding to finfo.
|
||||
// It's equivalent to v.FieldByIndex(finfo.idx), but initializes
|
||||
// and dereferences pointers as necessary.
|
||||
func (finfo *fieldInfo) value(v reflect.Value) reflect.Value {
|
||||
for i, x := range finfo.idx {
|
||||
if i > 0 {
|
||||
t := v.Type()
|
||||
if t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct {
|
||||
if v.IsNil() {
|
||||
v.Set(reflect.New(v.Type().Elem()))
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
}
|
||||
v = v.Field(x)
|
||||
}
|
||||
return v
|
||||
}
|
276
vendor/github.com/DHowett/go-plist/unmarshal.go
generated
vendored
|
@ -1,276 +0,0 @@
|
|||
package plist
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
type incompatibleDecodeTypeError struct {
|
||||
typ reflect.Type
|
||||
pKind plistKind
|
||||
}
|
||||
|
||||
func (u *incompatibleDecodeTypeError) Error() string {
|
||||
return fmt.Sprintf("plist: type mismatch: tried to decode %v into value of type %v", plistKindNames[u.pKind], u.typ)
|
||||
}
|
||||
|
||||
var (
|
||||
textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
|
||||
)
|
||||
|
||||
func isEmptyInterface(v reflect.Value) bool {
|
||||
return v.Kind() == reflect.Interface && v.NumMethod() == 0
|
||||
}
|
||||
|
||||
func (p *Decoder) unmarshalTextInterface(pval *plistValue, unmarshalable encoding.TextUnmarshaler) {
|
||||
err := unmarshalable.UnmarshalText([]byte(pval.value.(string)))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Decoder) unmarshalTime(pval *plistValue, val reflect.Value) {
|
||||
val.Set(reflect.ValueOf(pval.value.(time.Time)))
|
||||
}
|
||||
|
||||
func (p *Decoder) unmarshalLaxString(s string, val reflect.Value) {
|
||||
switch val.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
i := mustParseInt(s, 10, 64)
|
||||
val.SetInt(i)
|
||||
return
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
i := mustParseUint(s, 10, 64)
|
||||
val.SetUint(i)
|
||||
return
|
||||
case reflect.Float32, reflect.Float64:
|
||||
f := mustParseFloat(s, 64)
|
||||
val.SetFloat(f)
|
||||
return
|
||||
case reflect.Bool:
|
||||
b := mustParseBool(s)
|
||||
val.SetBool(b)
|
||||
return
|
||||
case reflect.Struct:
|
||||
if val.Type() == timeType {
|
||||
t, err := time.Parse(textPlistTimeLayout, s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
val.Set(reflect.ValueOf(t.In(time.UTC)))
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
panic(&incompatibleDecodeTypeError{val.Type(), String})
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Decoder) unmarshal(pval *plistValue, val reflect.Value) {
|
||||
if pval == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if val.Kind() == reflect.Ptr {
|
||||
if val.IsNil() {
|
||||
val.Set(reflect.New(val.Type().Elem()))
|
||||
}
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
if isEmptyInterface(val) {
|
||||
v := p.valueInterface(pval)
|
||||
val.Set(reflect.ValueOf(v))
|
||||
return
|
||||
}
|
||||
|
||||
incompatibleTypeError := &incompatibleDecodeTypeError{val.Type(), pval.kind}
|
||||
|
||||
// time.Time implements TextMarshaler, but we need to parse it as RFC3339
|
||||
if pval.kind == Date {
|
||||
if val.Type() == timeType {
|
||||
p.unmarshalTime(pval, val)
|
||||
return
|
||||
}
|
||||
panic(incompatibleTypeError)
|
||||
}
|
||||
|
||||
if val.CanInterface() && val.Type().Implements(textUnmarshalerType) && val.Type() != timeType {
|
||||
p.unmarshalTextInterface(pval, val.Interface().(encoding.TextUnmarshaler))
|
||||
return
|
||||
}
|
||||
|
||||
if val.CanAddr() {
|
||||
pv := val.Addr()
|
||||
if pv.CanInterface() && pv.Type().Implements(textUnmarshalerType) && val.Type() != timeType {
|
||||
p.unmarshalTextInterface(pval, pv.Interface().(encoding.TextUnmarshaler))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
typ := val.Type()
|
||||
|
||||
switch pval.kind {
|
||||
case String:
|
||||
if val.Kind() == reflect.String {
|
||||
val.SetString(pval.value.(string))
|
||||
return
|
||||
}
|
||||
if p.lax {
|
||||
p.unmarshalLaxString(pval.value.(string), val)
|
||||
return
|
||||
}
|
||||
|
||||
panic(incompatibleTypeError)
|
||||
case Integer:
|
||||
switch val.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
val.SetInt(int64(pval.value.(signedInt).value))
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
val.SetUint(pval.value.(signedInt).value)
|
||||
default:
|
||||
panic(incompatibleTypeError)
|
||||
}
|
||||
case Real:
|
||||
if val.Kind() == reflect.Float32 || val.Kind() == reflect.Float64 {
|
||||
val.SetFloat(pval.value.(sizedFloat).value)
|
||||
} else {
|
||||
panic(incompatibleTypeError)
|
||||
}
|
||||
case Boolean:
|
||||
if val.Kind() == reflect.Bool {
|
||||
val.SetBool(pval.value.(bool))
|
||||
} else {
|
||||
panic(incompatibleTypeError)
|
||||
}
|
||||
case Data:
|
||||
if val.Kind() == reflect.Slice && typ.Elem().Kind() == reflect.Uint8 {
|
||||
val.SetBytes(pval.value.([]byte))
|
||||
} else {
|
||||
panic(incompatibleTypeError)
|
||||
}
|
||||
case Array:
|
||||
p.unmarshalArray(pval, val)
|
||||
case Dictionary:
|
||||
p.unmarshalDictionary(pval, val)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Decoder) unmarshalArray(pval *plistValue, val reflect.Value) {
|
||||
subvalues := pval.value.([]*plistValue)
|
||||
|
||||
var n int
|
||||
if val.Kind() == reflect.Slice {
|
||||
// Slice of element values.
|
||||
// Grow slice.
|
||||
cnt := len(subvalues) + val.Len()
|
||||
if cnt >= val.Cap() {
|
||||
ncap := 2 * cnt
|
||||
if ncap < 4 {
|
||||
ncap = 4
|
||||
}
|
||||
new := reflect.MakeSlice(val.Type(), val.Len(), ncap)
|
||||
reflect.Copy(new, val)
|
||||
val.Set(new)
|
||||
}
|
||||
n = val.Len()
|
||||
val.SetLen(cnt)
|
||||
} else if val.Kind() == reflect.Array {
|
||||
if len(subvalues) > val.Cap() {
|
||||
panic(fmt.Errorf("plist: attempted to unmarshal %d values into an array of size %d", len(subvalues), val.Cap()))
|
||||
}
|
||||
} else {
|
||||
panic(&incompatibleDecodeTypeError{val.Type(), pval.kind})
|
||||
}
|
||||
|
||||
// Recur to read element into slice.
|
||||
for _, sval := range subvalues {
|
||||
p.unmarshal(sval, val.Index(n))
|
||||
n++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Decoder) unmarshalDictionary(pval *plistValue, val reflect.Value) {
|
||||
typ := val.Type()
|
||||
switch val.Kind() {
|
||||
case reflect.Struct:
|
||||
tinfo, err := getTypeInfo(typ)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
subvalues := pval.value.(*dictionary).m
|
||||
for _, finfo := range tinfo.fields {
|
||||
p.unmarshal(subvalues[finfo.name], finfo.value(val))
|
||||
}
|
||||
case reflect.Map:
|
||||
if val.IsNil() {
|
||||
val.Set(reflect.MakeMap(typ))
|
||||
}
|
||||
|
||||
subvalues := pval.value.(*dictionary).m
|
||||
for k, sval := range subvalues {
|
||||
keyv := reflect.ValueOf(k).Convert(typ.Key())
|
||||
mapElem := val.MapIndex(keyv)
|
||||
if !mapElem.IsValid() {
|
||||
mapElem = reflect.New(typ.Elem()).Elem()
|
||||
}
|
||||
|
||||
p.unmarshal(sval, mapElem)
|
||||
val.SetMapIndex(keyv, mapElem)
|
||||
}
|
||||
default:
|
||||
panic(&incompatibleDecodeTypeError{typ, pval.kind})
|
||||
}
|
||||
}
|
||||
|
||||
/* *Interface is modelled after encoding/json */
|
||||
func (p *Decoder) valueInterface(pval *plistValue) interface{} {
|
||||
switch pval.kind {
|
||||
case String:
|
||||
return pval.value.(string)
|
||||
case Integer:
|
||||
if pval.value.(signedInt).signed {
|
||||
return int64(pval.value.(signedInt).value)
|
||||
}
|
||||
return pval.value.(signedInt).value
|
||||
case Real:
|
||||
bits := pval.value.(sizedFloat).bits
|
||||
switch bits {
|
||||
case 32:
|
||||
return float32(pval.value.(sizedFloat).value)
|
||||
case 64:
|
||||
return pval.value.(sizedFloat).value
|
||||
}
|
||||
case Boolean:
|
||||
return pval.value.(bool)
|
||||
case Array:
|
||||
return p.arrayInterface(pval.value.([]*plistValue))
|
||||
case Dictionary:
|
||||
return p.dictionaryInterface(pval.value.(*dictionary))
|
||||
case Data:
|
||||
return pval.value.([]byte)
|
||||
case Date:
|
||||
return pval.value.(time.Time)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Decoder) arrayInterface(subvalues []*plistValue) []interface{} {
|
||||
out := make([]interface{}, len(subvalues))
|
||||
for i, subv := range subvalues {
|
||||
out[i] = p.valueInterface(subv)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (p *Decoder) dictionaryInterface(dict *dictionary) map[string]interface{} {
|
||||
out := make(map[string]interface{})
|
||||
for k, subv := range dict.m {
|
||||
out[k] = p.valueInterface(subv)
|
||||
}
|
||||
return out
|
||||
}
|
18
vendor/github.com/DHowett/go-plist/util.go
generated
vendored
|
@ -1,18 +0,0 @@
|
|||
package plist
|
||||
|
||||
import "io"
|
||||
|
||||
type countedWriter struct {
|
||||
io.Writer
|
||||
nbytes int
|
||||
}
|
||||
|
||||
func (w *countedWriter) Write(p []byte) (int, error) {
|
||||
n, err := w.Writer.Write(p)
|
||||
w.nbytes += n
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (w *countedWriter) BytesWritten() int {
|
||||
return w.nbytes
|
||||
}
|
314
vendor/github.com/DHowett/go-plist/xml.go
generated
vendored
|
@ -1,314 +0,0 @@
|
|||
package plist
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const xmlDOCTYPE = `<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
`
|
||||
|
||||
type xmlPlistGenerator struct {
|
||||
writer io.Writer
|
||||
xmlEncoder *xml.Encoder
|
||||
}
|
||||
|
||||
func (p *xmlPlistGenerator) generateDocument(pval *plistValue) {
|
||||
io.WriteString(p.writer, xml.Header)
|
||||
io.WriteString(p.writer, xmlDOCTYPE)
|
||||
|
||||
plistStartElement := xml.StartElement{
|
||||
Name: xml.Name{
|
||||
Space: "",
|
||||
Local: "plist",
|
||||
},
|
||||
Attr: []xml.Attr{{
|
||||
Name: xml.Name{
|
||||
Space: "",
|
||||
Local: "version"},
|
||||
Value: "1.0"},
|
||||
},
|
||||
}
|
||||
|
||||
p.xmlEncoder.EncodeToken(plistStartElement)
|
||||
|
||||
p.writePlistValue(pval)
|
||||
|
||||
p.xmlEncoder.EncodeToken(plistStartElement.End())
|
||||
p.xmlEncoder.Flush()
|
||||
}
|
||||
|
||||
func (p *xmlPlistGenerator) writePlistValue(pval *plistValue) {
|
||||
if pval == nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer p.xmlEncoder.Flush()
|
||||
|
||||
key := ""
|
||||
encodedValue := pval.value
|
||||
switch pval.kind {
|
||||
case Dictionary:
|
||||
startElement := xml.StartElement{Name: xml.Name{Local: "dict"}}
|
||||
p.xmlEncoder.EncodeToken(startElement)
|
||||
dict := encodedValue.(*dictionary)
|
||||
dict.populateArrays()
|
||||
for i, k := range dict.keys {
|
||||
p.xmlEncoder.EncodeElement(k, xml.StartElement{Name: xml.Name{Local: "key"}})
|
||||
p.writePlistValue(dict.values[i])
|
||||
}
|
||||
p.xmlEncoder.EncodeToken(startElement.End())
|
||||
case Array:
|
||||
startElement := xml.StartElement{Name: xml.Name{Local: "array"}}
|
||||
p.xmlEncoder.EncodeToken(startElement)
|
||||
values := encodedValue.([]*plistValue)
|
||||
for _, v := range values {
|
||||
p.writePlistValue(v)
|
||||
}
|
||||
p.xmlEncoder.EncodeToken(startElement.End())
|
||||
case String:
|
||||
key = "string"
|
||||
case Integer:
|
||||
key = "integer"
|
||||
if pval.value.(signedInt).signed {
|
||||
encodedValue = int64(pval.value.(signedInt).value)
|
||||
} else {
|
||||
encodedValue = pval.value.(signedInt).value
|
||||
}
|
||||
case Real:
|
||||
key = "real"
|
||||
encodedValue = pval.value.(sizedFloat).value
|
||||
switch {
|
||||
case math.IsInf(pval.value.(sizedFloat).value, 1):
|
||||
encodedValue = "inf"
|
||||
case math.IsInf(pval.value.(sizedFloat).value, -1):
|
||||
encodedValue = "-inf"
|
||||
case math.IsNaN(pval.value.(sizedFloat).value):
|
||||
encodedValue = "nan"
|
||||
}
|
||||
case Boolean:
|
||||
key = "false"
|
||||
b := pval.value.(bool)
|
||||
if b {
|
||||
key = "true"
|
||||
}
|
||||
encodedValue = ""
|
||||
case Data:
|
||||
key = "data"
|
||||
encodedValue = xml.CharData(base64.StdEncoding.EncodeToString(pval.value.([]byte)))
|
||||
case Date:
|
||||
key = "date"
|
||||
encodedValue = pval.value.(time.Time).In(time.UTC).Format(time.RFC3339)
|
||||
}
|
||||
if key != "" {
|
||||
err := p.xmlEncoder.EncodeElement(encodedValue, xml.StartElement{Name: xml.Name{Local: key}})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *xmlPlistGenerator) Indent(i string) {
|
||||
p.xmlEncoder.Indent("", i)
|
||||
}
|
||||
|
||||
func newXMLPlistGenerator(w io.Writer) *xmlPlistGenerator {
|
||||
mw := mustWriter{w}
|
||||
return &xmlPlistGenerator{mw, xml.NewEncoder(mw)}
|
||||
}
|
||||
|
||||
type xmlPlistParser struct {
|
||||
reader io.Reader
|
||||
xmlDecoder *xml.Decoder
|
||||
whitespaceReplacer *strings.Replacer
|
||||
ntags int
|
||||
}
|
||||
|
||||
func (p *xmlPlistParser) parseDocument() (pval *plistValue, parseError error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if _, ok := r.(runtime.Error); ok {
|
||||
panic(r)
|
||||
}
|
||||
if _, ok := r.(invalidPlistError); ok {
|
||||
parseError = r.(error)
|
||||
} else {
|
||||
// Wrap all non-invalid-plist errors.
|
||||
parseError = plistParseError{"XML", r.(error)}
|
||||
}
|
||||
}
|
||||
}()
|
||||
for {
|
||||
if token, err := p.xmlDecoder.Token(); err == nil {
|
||||
if element, ok := token.(xml.StartElement); ok {
|
||||
pval = p.parseXMLElement(element)
|
||||
if p.ntags == 0 {
|
||||
panic(invalidPlistError{"XML", errors.New("no elements encountered")})
|
||||
}
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// The first XML parse turned out to be invalid:
|
||||
// we do not have an XML property list.
|
||||
panic(invalidPlistError{"XML", err})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *xmlPlistParser) parseXMLElement(element xml.StartElement) *plistValue {
|
||||
var charData xml.CharData
|
||||
switch element.Name.Local {
|
||||
case "plist":
|
||||
p.ntags++
|
||||
for {
|
||||
token, err := p.xmlDecoder.Token()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if el, ok := token.(xml.EndElement); ok && el.Name.Local == "plist" {
|
||||
break
|
||||
}
|
||||
|
||||
if el, ok := token.(xml.StartElement); ok {
|
||||
return p.parseXMLElement(el)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
case "string":
|
||||
p.ntags++
|
||||
err := p.xmlDecoder.DecodeElement(&charData, &element)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &plistValue{String, string(charData)}
|
||||
case "integer":
|
||||
p.ntags++
|
||||
err := p.xmlDecoder.DecodeElement(&charData, &element)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
s := string(charData)
|
||||
if s[0] == '-' {
|
||||
n := mustParseInt(string(charData), 10, 64)
|
||||
return &plistValue{Integer, signedInt{uint64(n), true}}
|
||||
} else {
|
||||
n := mustParseUint(string(charData), 10, 64)
|
||||
return &plistValue{Integer, signedInt{n, false}}
|
||||
}
|
||||
case "real":
|
||||
p.ntags++
|
||||
err := p.xmlDecoder.DecodeElement(&charData, &element)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
n := mustParseFloat(string(charData), 64)
|
||||
return &plistValue{Real, sizedFloat{n, 64}}
|
||||
case "true", "false":
|
||||
p.ntags++
|
||||
p.xmlDecoder.Skip()
|
||||
|
||||
b := element.Name.Local == "true"
|
||||
return &plistValue{Boolean, b}
|
||||
case "date":
|
||||
p.ntags++
|
||||
err := p.xmlDecoder.DecodeElement(&charData, &element)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
t, err := time.ParseInLocation(time.RFC3339, string(charData), time.UTC)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &plistValue{Date, t}
|
||||
case "data":
|
||||
p.ntags++
|
||||
err := p.xmlDecoder.DecodeElement(&charData, &element)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
str := p.whitespaceReplacer.Replace(string(charData))
|
||||
|
||||
l := base64.StdEncoding.DecodedLen(len(str))
|
||||
bytes := make([]uint8, l)
|
||||
l, err = base64.StdEncoding.Decode(bytes, []byte(str))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &plistValue{Data, bytes[:l]}
|
||||
case "dict":
|
||||
p.ntags++
|
||||
var key *string
|
||||
var subvalues map[string]*plistValue = make(map[string]*plistValue)
|
||||
for {
|
||||
token, err := p.xmlDecoder.Token()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if el, ok := token.(xml.EndElement); ok && el.Name.Local == "dict" {
|
||||
if key != nil {
|
||||
panic(errors.New("missing value in dictionary"))
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if el, ok := token.(xml.StartElement); ok {
|
||||
if el.Name.Local == "key" {
|
||||
var k string
|
||||
p.xmlDecoder.DecodeElement(&k, &el)
|
||||
key = &k
|
||||
} else {
|
||||
if key == nil {
|
||||
panic(errors.New("missing key in dictionary"))
|
||||
}
|
||||
subvalues[*key] = p.parseXMLElement(el)
|
||||
key = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return &plistValue{Dictionary, &dictionary{m: subvalues}}
|
||||
case "array":
|
||||
p.ntags++
|
||||
var subvalues []*plistValue = make([]*plistValue, 0, 10)
|
||||
for {
|
||||
token, err := p.xmlDecoder.Token()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if el, ok := token.(xml.EndElement); ok && el.Name.Local == "array" {
|
||||
break
|
||||
}
|
||||
|
||||
if el, ok := token.(xml.StartElement); ok {
|
||||
subvalues = append(subvalues, p.parseXMLElement(el))
|
||||
}
|
||||
}
|
||||
return &plistValue{Array, subvalues}
|
||||
}
|
||||
err := fmt.Errorf("encountered unknown element %s", element.Name.Local)
|
||||
if p.ntags == 0 {
|
||||
// If out first XML tag is invalid, it might be an openstep data element, ala <abab> or <0101>
|
||||
panic(invalidPlistError{"XML", err})
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
|
||||
func newXMLPlistParser(r io.Reader) *xmlPlistParser {
|
||||
return &xmlPlistParser{r, xml.NewDecoder(r), strings.NewReplacer("\t", "", "\n", "", " ", "", "\r", ""), 0}
|
||||
}
|
4
vendor/github.com/alecthomas/kingpin/.travis.yml
generated
vendored
|
@ -1,4 +0,0 @@
|
|||
sudo: false
|
||||
language: go
|
||||
install: go get -t -v ./...
|
||||
go: 1.2
|
19
vendor/github.com/alecthomas/kingpin/COPYING
generated
vendored
|
@ -1,19 +0,0 @@
|
|||
Copyright (C) 2014 Alec Thomas
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
560
vendor/github.com/alecthomas/kingpin/README.md
generated
vendored
|
@ -1,560 +0,0 @@
|
|||
# Kingpin - A Go (golang) command line and flag parser [![Build Status](https://travis-ci.org/alecthomas/kingpin.png)](https://travis-ci.org/alecthomas/kingpin)
|
||||
|
||||
<!-- MarkdownTOC -->
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Features](#features)
|
||||
- [User-visible changes between v1 and v2](#user-visible-changes-between-v1-and-v2)
|
||||
- [Flags can be used at any point after their definition.](#flags-can-be-used-at-any-point-after-their-definition)
|
||||
- [Short flags can be combined with their parameters](#short-flags-can-be-combined-with-their-parameters)
|
||||
- [API changes between v1 and v2](#api-changes-between-v1-and-v2)
|
||||
- [Versions](#versions)
|
||||
- [V2 is the current stable version](#v2-is-the-current-stable-version)
|
||||
- [V1 is the OLD stable version](#v1-is-the-old-stable-version)
|
||||
- [Change History](#change-history)
|
||||
- [Examples](#examples)
|
||||
- [Simple Example](#simple-example)
|
||||
- [Complex Example](#complex-example)
|
||||
- [Reference Documentation](#reference-documentation)
|
||||
- [Displaying errors and usage information](#displaying-errors-and-usage-information)
|
||||
- [Sub-commands](#sub-commands)
|
||||
- [Custom Parsers](#custom-parsers)
|
||||
- [Default Values](#default-values)
|
||||
- [Place-holders in Help](#place-holders-in-help)
|
||||
- [Consuming all remaining arguments](#consuming-all-remaining-arguments)
|
||||
- [Supporting -h for help](#supporting--h-for-help)
|
||||
- [Custom help](#custom-help)
|
||||
|
||||
<!-- /MarkdownTOC -->
|
||||
|
||||
## Overview
|
||||
|
||||
Kingpin is a [fluent-style](http://en.wikipedia.org/wiki/Fluent_interface),
|
||||
type-safe command-line parser. It supports flags, nested commands, and
|
||||
positional arguments.
|
||||
|
||||
Install it with:
|
||||
|
||||
$ go get gopkg.in/alecthomas/kingpin.v2
|
||||
|
||||
It looks like this:
|
||||
|
||||
```go
|
||||
var (
|
||||
verbose = kingpin.Flag("verbose", "Verbose mode.").Short('v').Bool()
|
||||
name = kingpin.Arg("name", "Name of user.").Required().String()
|
||||
)
|
||||
|
||||
func main() {
|
||||
kingpin.Parse()
|
||||
fmt.Printf("%v, %s\n", *verbose, *name)
|
||||
}
|
||||
```
|
||||
|
||||
More [examples](https://github.com/alecthomas/kingpin/tree/master/examples) are available.
|
||||
|
||||
Second to parsing, providing the user with useful help is probably the most
|
||||
important thing a command-line parser does. Kingpin tries to provide detailed
|
||||
contextual help if `--help` is encountered at any point in the command line
|
||||
(excluding after `--`).
|
||||
|
||||
## Features
|
||||
|
||||
- Help output that isn't as ugly as sin.
|
||||
- Fully [customisable help](#custom-help), via Go templates.
|
||||
- Parsed, type-safe flags (`kingpin.Flag("f", "help").Int()`)
|
||||
- Parsed, type-safe positional arguments (`kingpin.Arg("a", "help").Int()`).
|
||||
- Parsed, type-safe, arbitrarily deep commands (`kingpin.Command("c", "help")`).
|
||||
- Support for required flags and required positional arguments (`kingpin.Flag("f", "").Required().Int()`).
|
||||
- Support for arbitrarily nested default commands (`command.Default()`).
|
||||
- Callbacks per command, flag and argument (`kingpin.Command("c", "").Action(myAction)`).
|
||||
- POSIX-style short flag combining (`-a -b` -> `-ab`).
|
||||
- Short-flag+parameter combining (`-a parm` -> `-aparm`).
|
||||
- Read command-line from files (`@<file>`).
|
||||
- Automatically generate man pages (`--man-page`).
|
||||
|
||||
## User-visible changes between v1 and v2
|
||||
|
||||
### Flags can be used at any point after their definition.
|
||||
|
||||
Flags can be specified at any point after their definition, not just
|
||||
*immediately after their associated command*. From the chat example below, the
|
||||
following used to be required:
|
||||
|
||||
```
|
||||
$ chat --server=chat.server.com:8080 post --image=~/Downloads/owls.jpg pics
|
||||
```
|
||||
|
||||
But the following will now work:
|
||||
|
||||
```
|
||||
$ chat post --server=chat.server.com:8080 --image=~/Downloads/owls.jpg pics
|
||||
```
|
||||
|
||||
### Short flags can be combined with their parameters
|
||||
|
||||
Previously, if a short flag was used, any argument to that flag would have to
|
||||
be separated by a space. That is no longer the case.
|
||||
|
||||
## API changes between v1 and v2
|
||||
|
||||
- `ParseWithFileExpansion()` is gone. The new parser directly supports expanding `@<file>`.
|
||||
- Added `FatalUsage()` and `FatalUsageContext()` for displaying an error + usage and terminating.
|
||||
- `Dispatch()` renamed to `Action()`.
|
||||
- Added `ParseContext()` for parsing a command line into its intermediate context form without executing.
|
||||
- Added `Terminate()` function to override the termination function.
|
||||
- Added `UsageForContextWithTemplate()` for printing usage via a custom template.
|
||||
- Added `UsageTemplate()` for overriding the default template to use. Two templates are included:
|
||||
1. `DefaultUsageTemplate` - default template.
|
||||
2. `CompactUsageTemplate` - compact command template for larger applications.
|
||||
|
||||
## Versions
|
||||
|
||||
Kingpin uses [gopkg.in](https://gopkg.in/alecthomas/kingpin) for versioning.
|
||||
|
||||
The current stable version is [gopkg.in/alecthomas/kingpin.v2](https://gopkg.in/alecthomas/kingpin.v2). The previous version, [gopkg.in/alecthomas/kingpin.v1](https://gopkg.in/alecthomas/kingpin.v1), is deprecated and in maintenance mode.
|
||||
|
||||
### [V2](https://gopkg.in/alecthomas/kingpin.v2) is the current stable version
|
||||
|
||||
Installation:
|
||||
|
||||
```sh
|
||||
$ go get gopkg.in/alecthomas/kingpin.v2
|
||||
```
|
||||
|
||||
### [V1](https://gopkg.in/alecthomas/kingpin.v1) is the OLD stable version
|
||||
|
||||
Installation:
|
||||
|
||||
```sh
|
||||
$ go get gopkg.in/alecthomas/kingpin.v1
|
||||
```
|
||||
|
||||
## Change History
|
||||
|
||||
- *2015-09-19* -- Stable v2.1.0 release.
|
||||
- Added `command.Default()` to specify a default command to use if no other
|
||||
command matches. This allows for convenient user shortcuts.
|
||||
- Exposed `HelpFlag` and `VersionFlag` for further cusomisation.
|
||||
- `Action()` and `PreAction()` added and both now support an arbitrary
|
||||
number of callbacks.
|
||||
- `kingpin.SeparateOptionalFlagsUsageTemplate`.
|
||||
- `--help-long` and `--help-man` (hidden by default) flags.
|
||||
- Flags are "interspersed" by default, but can be disabled with `app.Interspersed(false)`.
|
||||
- Added flags for all simple builtin types (int8, uint16, etc.) and slice variants.
|
||||
- Use `app.Writer(os.Writer)` to specify the default writer for all output functions.
|
||||
- Dropped `os.Writer` prefix from all printf-like functions.
|
||||
|
||||
- *2015-05-22* -- Stable v2.0.0 release.
|
||||
- Initial stable release of v2.0.0.
|
||||
- Fully supports interspersed flags, commands and arguments.
|
||||
- Flags can be present at any point after their logical definition.
|
||||
- Application.Parse() terminates if commands are present and a command is not parsed.
|
||||
- Dispatch() -> Action().
|
||||
- Actions are dispatched after all values are populated.
|
||||
- Override termination function (defaults to os.Exit).
|
||||
- Override output stream (defaults to os.Stderr).
|
||||
- Templatised usage help, with default and compact templates.
|
||||
- Make error/usage functions more consistent.
|
||||
- Support argument expansion from files by default (with @<file>).
|
||||
- Fully public data model is available via .Model().
|
||||
- Parser has been completely refactored.
|
||||
- Parsing and execution has been split into distinct stages.
|
||||
- Use `go generate` to generate repeated flags.
|
||||
- Support combined short-flag+argument: -fARG.
|
||||
|
||||
- *2015-01-23* -- Stable v1.3.4 release.
|
||||
- Support "--" for separating flags from positional arguments.
|
||||
- Support loading flags from files (ParseWithFileExpansion()). Use @FILE as an argument.
|
||||
- Add post-app and post-cmd validation hooks. This allows arbitrary validation to be added.
|
||||
- A bunch of improvements to help usage and formatting.
|
||||
- Support arbitrarily nested sub-commands.
|
||||
|
||||
- *2014-07-08* -- Stable v1.2.0 release.
|
||||
- Pass any value through to `Strings()` when final argument.
|
||||
Allows for values that look like flags to be processed.
|
||||
- Allow `--help` to be used with commands.
|
||||
- Support `Hidden()` flags.
|
||||
- Parser for [units.Base2Bytes](https://github.com/alecthomas/units)
|
||||
type. Allows for flags like `--ram=512MB` or `--ram=1GB`.
|
||||
- Add an `Enum()` value, allowing only one of a set of values
|
||||
to be selected. eg. `Flag(...).Enum("debug", "info", "warning")`.
|
||||
|
||||
- *2014-06-27* -- Stable v1.1.0 release.
|
||||
- Bug fixes.
|
||||
- Always return an error (rather than panicing) when misconfigured.
|
||||
- `OpenFile(flag, perm)` value type added, for finer control over opening files.
|
||||
- Significantly improved usage formatting.
|
||||
|
||||
- *2014-06-19* -- Stable v1.0.0 release.
|
||||
- Support [cumulative positional](#consuming-all-remaining-arguments) arguments.
|
||||
- Return error rather than panic when there are fatal errors not caught by
|
||||
the type system. eg. when a default value is invalid.
|
||||
- Use gokpg.in.
|
||||
|
||||
- *2014-06-10* -- Place-holder streamlining.
|
||||
- Renamed `MetaVar` to `PlaceHolder`.
|
||||
- Removed `MetaVarFromDefault`. Kingpin now uses [heuristics](#place-holders-in-help)
|
||||
to determine what to display.
|
||||
|
||||
## Examples
|
||||
|
||||
### Simple Example
|
||||
|
||||
Kingpin can be used for simple flag+arg applications like so:
|
||||
|
||||
```
|
||||
$ ping --help
|
||||
usage: ping [<flags>] <ip> [<count>]
|
||||
|
||||
Flags:
|
||||
--debug Enable debug mode.
|
||||
--help Show help.
|
||||
-t, --timeout=5s Timeout waiting for ping.
|
||||
|
||||
Args:
|
||||
<ip> IP address to ping.
|
||||
[<count>] Number of packets to send
|
||||
$ ping 1.2.3.4 5
|
||||
Would ping: 1.2.3.4 with timeout 5s and count 0
|
||||
```
|
||||
|
||||
From the following source:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
debug = kingpin.Flag("debug", "Enable debug mode.").Bool()
|
||||
timeout = kingpin.Flag("timeout", "Timeout waiting for ping.").Default("5s").OverrideDefaultFromEnvar("PING_TIMEOUT").Short('t').Duration()
|
||||
ip = kingpin.Arg("ip", "IP address to ping.").Required().IP()
|
||||
count = kingpin.Arg("count", "Number of packets to send").Int()
|
||||
)
|
||||
|
||||
func main() {
|
||||
kingpin.Version("0.0.1")
|
||||
kingpin.Parse()
|
||||
fmt.Printf("Would ping: %s with timeout %s and count %d", *ip, *timeout, *count)
|
||||
}
|
||||
```
|
||||
|
||||
### Complex Example
|
||||
|
||||
Kingpin can also produce complex command-line applications with global flags,
|
||||
subcommands, and per-subcommand flags, like this:
|
||||
|
||||
```
|
||||
$ chat --help
|
||||
usage: chat [<flags>] <command> [<flags>] [<args> ...]
|
||||
|
||||
A command-line chat application.
|
||||
|
||||
Flags:
|
||||
--help Show help.
|
||||
--debug Enable debug mode.
|
||||
--server=127.0.0.1 Server address.
|
||||
|
||||
Commands:
|
||||
help [<command>]
|
||||
Show help for a command.
|
||||
|
||||
register <nick> <name>
|
||||
Register a new user.
|
||||
|
||||
post [<flags>] <channel> [<text>]
|
||||
Post a message to a channel.
|
||||
|
||||
$ chat help post
|
||||
usage: chat [<flags>] post [<flags>] <channel> [<text>]
|
||||
|
||||
Post a message to a channel.
|
||||
|
||||
Flags:
|
||||
--image=IMAGE Image to post.
|
||||
|
||||
Args:
|
||||
<channel> Channel to post to.
|
||||
[<text>] Text to post.
|
||||
|
||||
$ chat post --image=~/Downloads/owls.jpg pics
|
||||
...
|
||||
```
|
||||
|
||||
From this code:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
app = kingpin.New("chat", "A command-line chat application.")
|
||||
debug = app.Flag("debug", "Enable debug mode.").Bool()
|
||||
serverIP = app.Flag("server", "Server address.").Default("127.0.0.1").IP()
|
||||
|
||||
register = app.Command("register", "Register a new user.")
|
||||
registerNick = register.Arg("nick", "Nickname for user.").Required().String()
|
||||
registerName = register.Arg("name", "Name of user.").Required().String()
|
||||
|
||||
post = app.Command("post", "Post a message to a channel.")
|
||||
postImage = post.Flag("image", "Image to post.").File()
|
||||
postChannel = post.Arg("channel", "Channel to post to.").Required().String()
|
||||
postText = post.Arg("text", "Text to post.").Strings()
|
||||
)
|
||||
|
||||
func main() {
|
||||
switch kingpin.MustParse(app.Parse(os.Args[1:])) {
|
||||
// Register user
|
||||
case register.FullCommand():
|
||||
println(*registerNick)
|
||||
|
||||
// Post message
|
||||
case post.FullCommand():
|
||||
if *postImage != nil {
|
||||
}
|
||||
text := strings.Join(*postText, " ")
|
||||
println("Post:", text)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Reference Documentation
|
||||
|
||||
### Displaying errors and usage information
|
||||
|
||||
Kingpin exports a set of functions to provide consistent errors and usage
|
||||
information to the user.
|
||||
|
||||
Error messages look something like this:
|
||||
|
||||
<app>: error: <message>
|
||||
|
||||
The functions on `Application` are:
|
||||
|
||||
Function | Purpose
|
||||
---------|--------------
|
||||
`Errorf(format, args)` | Display a printf formatted error to the user.
|
||||
`Fatalf(format, args)` | As with Errorf, but also call the termination handler.
|
||||
`FatalUsage(format, args)` | As with Fatalf, but also print contextual usage information.
|
||||
`FatalUsageContext(context, format, args)` | As with Fatalf, but also print contextual usage information from a `ParseContext`.
|
||||
`FatalIfError(err, format, args)` | Conditionally print an error prefixed with format+args, then call the termination handler
|
||||
|
||||
There are equivalent global functions in the kingpin namespace for the default
|
||||
`kingpin.CommandLine` instance.
|
||||
|
||||
### Sub-commands
|
||||
|
||||
Kingpin supports nested sub-commands, with separate flag and positional
|
||||
arguments per sub-command. Note that positional arguments may only occur after
|
||||
sub-commands.
|
||||
|
||||
For example:
|
||||
|
||||
```go
|
||||
var (
|
||||
deleteCommand = kingpin.Command("delete", "Delete an object.")
|
||||
deleteUserCommand = deleteCommand.Command("user", "Delete a user.")
|
||||
deleteUserUIDFlag = deleteUserCommand.Flag("uid", "Delete user by UID rather than username.")
|
||||
deleteUserUsername = deleteUserCommand.Arg("username", "Username to delete.")
|
||||
deletePostCommand = deleteCommand.Command("post", "Delete a post.")
|
||||
)
|
||||
|
||||
func main() {
|
||||
switch kingpin.Parse() {
|
||||
case "delete user":
|
||||
case "delete post":
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Parsers
|
||||
|
||||
Kingpin supports both flag and positional argument parsers for converting to
|
||||
Go types. For example, some included parsers are `Int()`, `Float()`,
|
||||
`Duration()` and `ExistingFile()`.
|
||||
|
||||
Parsers conform to Go's [`flag.Value`](http://godoc.org/flag#Value)
|
||||
interface, so any existing implementations will work.
|
||||
|
||||
For example, a parser for accumulating HTTP header values might look like this:
|
||||
|
||||
```go
|
||||
type HTTPHeaderValue http.Header
|
||||
|
||||
func (h *HTTPHeaderValue) Set(value string) error {
|
||||
parts := strings.SplitN(value, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("expected HEADER:VALUE got '%s'", value)
|
||||
}
|
||||
(*http.Header)(h).Add(parts[0], parts[1])
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HTTPHeaderValue) String() string {
|
||||
return ""
|
||||
}
|
||||
```
|
||||
|
||||
As a convenience, I would recommend something like this:
|
||||
|
||||
```go
|
||||
func HTTPHeader(s Settings) (target *http.Header) {
|
||||
target = new(http.Header)
|
||||
s.SetValue((*HTTPHeaderValue)(target))
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
You would use it like so:
|
||||
|
||||
```go
|
||||
headers = HTTPHeader(kingpin.Flag("header", "Add a HTTP header to the request.").Short('H'))
|
||||
```
|
||||
|
||||
### Default Values
|
||||
|
||||
The default value is the zero value for a type. This can be overridden with
|
||||
the `Default(value)` function on flags and arguments. This function accepts a
|
||||
string, which is parsed by the value itself, so it *must* be compliant with
|
||||
the format expected.
|
||||
|
||||
### Place-holders in Help
|
||||
|
||||
The place-holder value for a flag is the value used in the help to describe
|
||||
the value of a non-boolean flag.
|
||||
|
||||
The value provided to PlaceHolder() is used if provided, then the value
|
||||
provided by Default() if provided, then finally the capitalised flag name is
|
||||
used.
|
||||
|
||||
Here are some examples of flags with various permutations:
|
||||
|
||||
--name=NAME // Flag(...).String()
|
||||
--name="Harry" // Flag(...).Default("Harry").String()
|
||||
--name=FULL-NAME // flag(...).PlaceHolder("FULL-NAME").Default("Harry").String()
|
||||
|
||||
### Consuming all remaining arguments
|
||||
|
||||
A common command-line idiom is to use all remaining arguments for some
|
||||
purpose. eg. The following command accepts an arbitrary number of
|
||||
IP addresses as positional arguments:
|
||||
|
||||
./cmd ping 10.1.1.1 192.168.1.1
|
||||
|
||||
Kingpin supports this by having `Value` provide a `IsCumulative() bool`
|
||||
function. If this function exists and returns true, the value parser will be
|
||||
called repeatedly for every remaining argument.
|
||||
|
||||
Examples of this are the `Strings()` and `StringMap()` values.
|
||||
|
||||
To implement the above example we might do something like this:
|
||||
|
||||
```go
|
||||
type ipList []net.IP
|
||||
|
||||
func (i *ipList) Set(value string) error {
|
||||
if ip := net.ParseIP(value); ip == nil {
|
||||
return fmt.Errorf("'%s' is not an IP address", value)
|
||||
} else {
|
||||
*i = append(*i, ip)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (i *ipList) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (i *ipList) IsCumulative() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func IPList(s Settings) (target *[]net.IP) {
|
||||
target = new([]net.IP)
|
||||
s.SetValue((*ipList)(target))
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
And use it like so:
|
||||
|
||||
```go
|
||||
ips := IPList(kingpin.Arg("ips", "IP addresses to ping."))
|
||||
```
|
||||
|
||||
### Supporting -h for help
|
||||
|
||||
`kingpin.CommandLine.HelpFlag.Short('-h')`
|
||||
|
||||
### Custom help
|
||||
|
||||
Kingpin v2 supports templatised help using the text/template library (actually, [a fork](https://github.com/alecthomas/template)).
|
||||
|
||||
You can specify the template to use with the [Application.UsageTemplate()](http://godoc.org/gopkg.in/alecthomas/kingpin.v2#Application.UsageTemplate) function.
|
||||
|
||||
There are four included templates: `kingpin.DefaultUsageTemplate` is the default,
|
||||
`kingpin.CompactUsageTemplate` provides a more compact representation for more complex command-line structures,
|
||||
`kingpin.SeparateOptionalFlagsUsageTemplate` looks like the default template, but splits required
|
||||
and optional command flags into separate lists, and `kingpin.ManPageTemplate` is used to generate man pages.
|
||||
|
||||
See the above templates for examples of usage, and the the function [UsageForContextWithTemplate()](https://github.com/alecthomas/kingpin/blob/master/usage.go#L198) method for details on the context.
|
||||
|
||||
#### Default help template
|
||||
|
||||
```
|
||||
$ go run ./examples/curl/curl.go --help
|
||||
usage: curl [<flags>] <command> [<args> ...]
|
||||
|
||||
An example implementation of curl.
|
||||
|
||||
Flags:
|
||||
--help Show help.
|
||||
-t, --timeout=5s Set connection timeout.
|
||||
-H, --headers=HEADER=VALUE
|
||||
Add HTTP headers to the request.
|
||||
|
||||
Commands:
|
||||
help [<command>...]
|
||||
Show help.
|
||||
|
||||
get url <url>
|
||||
Retrieve a URL.
|
||||
|
||||
get file <file>
|
||||
Retrieve a file.
|
||||
|
||||
post [<flags>] <url>
|
||||
POST a resource.
|
||||
```
|
||||
|
||||
#### Compact help template
|
||||
|
||||
```
|
||||
$ go run ./examples/curl/curl.go --help
|
||||
usage: curl [<flags>] <command> [<args> ...]
|
||||
|
||||
An example implementation of curl.
|
||||
|
||||
Flags:
|
||||
--help Show help.
|
||||
-t, --timeout=5s Set connection timeout.
|
||||
-H, --headers=HEADER=VALUE
|
||||
Add HTTP headers to the request.
|
||||
|
||||
Commands:
|
||||
help [<command>...]
|
||||
get [<flags>]
|
||||
url <url>
|
||||
file <file>
|
||||
post [<flags>] <url>
|
||||
```
|