Compare commits
191 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdd6ff9435 | ||
|
|
29bd44ac0c | ||
|
|
4b9be2fd12 | ||
|
|
2d7a0f1aaf | ||
|
|
23f70c844a | ||
|
|
6d8af0af8f | ||
|
|
189100b3af | ||
|
|
75eb27f9c9 | ||
|
|
e31775af89 | ||
|
|
2a0e1f7400 | ||
|
|
dbd0e06c73 | ||
|
|
bd2d79953d | ||
|
|
dd8692b656 | ||
|
|
eacf2d1dc3 | ||
|
|
08e2cf764d | ||
|
|
a400a028ff | ||
|
|
f2fb06ae18 | ||
|
|
71957e4947 | ||
|
|
5fab10ed10 | ||
|
|
30e4b86595 | ||
|
|
5bcbcd0bac | ||
|
|
2a5610bb74 | ||
|
|
b86e0e623e | ||
|
|
bca7b842f5 | ||
|
|
ea7272437d | ||
|
|
6de7ca4676 | ||
|
|
88433ea55e | ||
|
|
d874ba59cf | ||
|
|
5dc0040375 | ||
|
|
034215a031 | ||
|
|
80c9e79869 | ||
|
|
2adc57be6c | ||
|
|
6829892be4 | ||
|
|
26545513de | ||
|
|
342d9cc14c | ||
|
|
ed85a9687b | ||
|
|
fe8dcd76c1 | ||
|
|
230bc3d182 | ||
|
|
28a90b90dd | ||
|
|
e607da8ec2 | ||
|
|
e47bdd1272 | ||
|
|
ff821dcb63 | ||
|
|
8887f4ef69 | ||
|
|
bd1a35541a | ||
|
|
56045c6c8e | ||
|
|
b5bd6779f9 | ||
|
|
8665999fc0 | ||
|
|
677c5a7cec | ||
|
|
41981a3d8a | ||
|
|
47789eec68 | ||
|
|
4c880193ce | ||
|
|
86aeecfac2 | ||
|
|
e99a12b6c3 | ||
|
|
e8af97872c | ||
|
|
d7590729f1 | ||
|
|
5a7724d542 | ||
|
|
f7063651c2 | ||
|
|
42d1e077c9 | ||
|
|
1258ac759f | ||
|
|
a60a9c4811 | ||
|
|
b6200a88c0 | ||
|
|
1618440f4f | ||
|
|
f676d25787 | ||
|
|
e24ff056f7 | ||
|
|
833443dfaf | ||
|
|
b6f8563d18 | ||
|
|
20ed231c53 | ||
|
|
b4470cf4bd | ||
|
|
bd4e9250c0 | ||
|
|
00940603ca | ||
|
|
621dc6c22f | ||
|
|
5367b9ea94 | ||
|
|
8e275e27be | ||
|
|
56e184748f | ||
|
|
b40bf8101f | ||
|
|
70e364c57b | ||
|
|
98b09cb27f | ||
|
|
aad95ae813 | ||
|
|
2bd7ec5cd0 | ||
|
|
85b2bd5dc4 | ||
|
|
fdec6aa9ac | ||
|
|
eaa82b8605 | ||
|
|
f4bdbeee73 | ||
|
|
905412e91b | ||
|
|
9318d19383 | ||
|
|
d62e765ae7 | ||
|
|
097ff92f17 | ||
|
|
fe28075b35 | ||
|
|
3339651d88 | ||
|
|
c8d67edf2c | ||
|
|
41ba422675 | ||
|
|
f9f3142543 | ||
|
|
9442e00997 | ||
|
|
1bbc103672 | ||
|
|
a16f689e99 | ||
|
|
6d5aae4dde | ||
|
|
9f9d518689 | ||
|
|
23441ea0c2 | ||
|
|
d22065f7e0 | ||
|
|
8e3ddf6f3c | ||
|
|
0fdd7d5906 | ||
|
|
e0ced3f803 | ||
|
|
261a04d83f | ||
|
|
d0fd2cc458 | ||
|
|
7056ab85f8 | ||
|
|
84d280a9a4 | ||
|
|
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 |
34
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: goreleaser
|
||||
on:
|
||||
push:
|
||||
# run only against tags
|
||||
tags:
|
||||
- '*'
|
||||
permissions:
|
||||
contents: write
|
||||
# packages: write
|
||||
# issues: write
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Fetch all tags
|
||||
run: git fetch --force --tags
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.18
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
with:
|
||||
# either 'goreleaser' (default) or 'goreleaser-pro'
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.TOKEN }}
|
||||
6
.gitignore
vendored
@@ -23,8 +23,12 @@ _testmain.go
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
dist/
|
||||
|
||||
gohttpserver
|
||||
bindata_assetfs.go
|
||||
assets_vfsdata.go
|
||||
*.un~
|
||||
*.swp
|
||||
|
||||
dist/
|
||||
.DS_Store
|
||||
|
||||
32
.goreleaser.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
version: 2
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
main: .
|
||||
ldflags: -s -w -X main.VERSION={{.Version}} -X main.BUILDTIME={{.Date}} -X main.GITCOMMIT={{.Commit}}
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
- "386"
|
||||
goarm:
|
||||
- "6"
|
||||
|
||||
archives:
|
||||
- format: tar.gz
|
||||
# use zip for windows archives
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
|
||||
# brews:
|
||||
# - tap:
|
||||
# owner: codeskyblue
|
||||
# name: homebrew-tap
|
||||
# folder: Formula
|
||||
@@ -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.
|
||||
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 shengxiang
|
||||
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
|
||||
|
||||
262
README.md
@@ -1,19 +1,19 @@
|
||||
# gohttpserver
|
||||
[](https://travis-ci.org/codeskyblue/gohttpserver)
|
||||
[](https://hub.docker.com/repository/docker/codeskyblue/gohttpserver)
|
||||
|
||||
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/)
|
||||
**Binaries** can be downloaded from [this repo 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
|
||||

|
||||
@@ -24,7 +24,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,33 +47,130 @@ 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
|
||||
```bash
|
||||
$ go install github.com/codeskyblue/gohttpserver@latest
|
||||
```
|
||||
go get -v github.com/codeskyblue/gohttpserver
|
||||
cd $GOPATH/src/github.com/codeskyblue/gohttpserver
|
||||
go build && ./gohttpserver
|
||||
|
||||
Or download binaries from [github releases](https://github.com/codeskyblue/gohttpserver/releases)
|
||||
|
||||
If you are using Mac, simply run command
|
||||
|
||||
```bash
|
||||
$ brew install codeskyblue/tap/gohttpserver
|
||||
```
|
||||
|
||||
## Usage
|
||||
Listen 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 codeskyblue/gohttpserver
|
||||
```
|
||||
|
||||
Share current directory with http basic auth
|
||||
|
||||
```bash
|
||||
$ docker run -it --rm -p 8000:8000 -v $PWD:/app/public --name gohttpserver \
|
||||
codeskyblue/gohttpserver \
|
||||
--auth-type http --auth-http username1:password1 --auth-http username2:password2
|
||||
```
|
||||
|
||||
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 \
|
||||
codeskyblue/gohttpserver \
|
||||
--auth-type openid
|
||||
```
|
||||
|
||||
To build image yourself, please change the PWD to the root of this repo.
|
||||
|
||||
```bash
|
||||
$ cd gohttpserver/
|
||||
$ docker build -t codeskyblue/gohttpserver -f docker/Dockerfile .
|
||||
```
|
||||
|
||||
## Authentication options
|
||||
- Enable basic http authentication
|
||||
|
||||
```sh
|
||||
$ gohttpserver --auth-type http --auth-http username1:password1 --auth-http username2:password2
|
||||
```
|
||||
|
||||
- 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 +181,142 @@ 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/
|
||||
```bash
|
||||
$ 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>
|
||||
|
||||
gohttpserver also support `--prefix` flag which will help to when meet `/` is occupied by other service. relative issue <https://github.com/codeskyblue/gohttpserver/issues/105>
|
||||
|
||||
Usage example:
|
||||
|
||||
```bash
|
||||
# for gohttpserver
|
||||
$ gohttpserver --prefix /foo --addr :8200 --xheaders
|
||||
```
|
||||
|
||||
**Nginx settigns**
|
||||
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain-name.com;
|
||||
|
||||
location /foo {
|
||||
proxy_pass http://127.0.0.1:8200; # here need to change
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
client_max_body_size 0; # disable upload limit
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## FAQ
|
||||
- [How to generate self signed certificate with openssl](http://stackoverflow.com/questions/10175812/how-to-create-a-self-signed-certificate-with-openssl)
|
||||
|
||||
### 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 build
|
||||
```
|
||||
|
||||
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 +327,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) Not using now
|
||||
* [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).
|
||||
|
||||
12
assets.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//go:embed assets
|
||||
var assetsFS embed.FS
|
||||
|
||||
// Assets contains project assets.
|
||||
var Assets = http.FS(assetsFS)
|
||||
|
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 |
306
assets/index.html
Normal file
@@ -0,0 +1,306 @@
|
||||
<!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="[[.Prefix]]/-/assets/favicon.png" />
|
||||
<link rel="stylesheet" type="text/css" href="[[.Prefix]]/-/assets/bootstrap-3.3.5/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="[[.Prefix]]/-/assets/font-awesome-4.6.3/css/font-awesome.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="[[.Prefix]]/-/assets/css/github-markdown.css">
|
||||
<link rel="stylesheet" type="text/css" href="[[.Prefix]]/-/assets/css/dropzone.css">
|
||||
<link rel="stylesheet" type="text/css" href="[[.Prefix]]/-/assets/css/scrollUp-image.css">
|
||||
<link rel="stylesheet" type="text/css" href="[[.Prefix]]/-/assets/css/style.css">
|
||||
<link rel="stylesheet" type="text/css" href="[[.Prefix]]/-/assets/themes/[[.Theme]].css">
|
||||
</head>
|
||||
|
||||
<body id="app">
|
||||
<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="[[.Prefix]]/">[[.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>
|
||||
|
||||
[[if not .NoIndex ]]
|
||||
<form class="navbar-form navbar-right">
|
||||
<div class="input-group">
|
||||
<input type="text" name="search" class="form-control" placeholder="Search text" v-bind:value="search"
|
||||
autofocus>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="submit">
|
||||
<span class="glyphicon glyphicon-search"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
[[end]]
|
||||
<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.name)}}">
|
||||
<!-- ?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.name)}}/?op=archive">
|
||||
<span class="hidden-xs">Archive</span> Zip
|
||||
<span class="glyphicon glyphicon-download-alt"></span>
|
||||
</a>
|
||||
<button class="btn btn-default btn-xs" v-on:click="showInfo(f)">
|
||||
<span class="glyphicon glyphicon-info-sign"></span>
|
||||
</button>
|
||||
<button class="btn btn-default btn-xs" v-if="auth.delete" v-on:click="deletePathConfirm(f, $event)">
|
||||
<span style="color:#CC3300" class="glyphicon glyphicon-trash"></span>
|
||||
</button>
|
||||
</template>
|
||||
<template v-if="f.type == 'file'">
|
||||
<a class="btn btn-default btn-xs hidden-xs" href="{{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/codeskyblue/gohttpserver">gohttpserver ({{version}})</a>, by <a href="https://github.com/codeskyblue">codeskyblue</a>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.URL_PFEFIX = "[[.Prefix]]"
|
||||
</script>
|
||||
<script src="[[.Prefix]]/-/assets/js/jquery-3.1.0.min.js"></script>
|
||||
<script src="[[.Prefix]]/-/assets/js/jquery.qrcode.js"></script>
|
||||
<script src="[[.Prefix]]/-/assets/js/jquery.scrollUp.min.js"></script>
|
||||
<script src="[[.Prefix]]/-/assets/js/qrcode.js"></script>
|
||||
<script src="[[.Prefix]]/-/assets/js/vue-1.0.min.js"></script>
|
||||
<script src="[[.Prefix]]/-/assets/js/showdown-1.6.4.min.js"></script>
|
||||
<script src="[[.Prefix]]/-/assets/js/moment.min.js"></script>
|
||||
<script src="[[.Prefix]]/-/assets/js/dropzone.js"></script>
|
||||
<script src="[[.Prefix]]/-/assets/js/underscore-min.js"></script>
|
||||
<script src="[[.Prefix]]/-/assets/js/clipboard-1.5.12.min.js"></script>
|
||||
<script src="[[.Prefix]]/-/assets/bootstrap-3.3.5/js/bootstrap.min.js"></script>
|
||||
<script src='[[.Prefix]]/-/assets/[["js/index.js" | urlhash ]]'></script>
|
||||
<!-- <script src="[[.Prefix]]/-/assets/js/index.js"></script> -->
|
||||
<!--Sync status bar color with border-color on mobile platforms.-->
|
||||
<script>
|
||||
var META = document.getElementsByTagName("meta");
|
||||
META[2]["content"]=$('.navbar').css('border-color');
|
||||
</script>
|
||||
[[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
461
assets/js/index.js
Normal file
@@ -0,0 +1,461 @@
|
||||
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 (filepath) {
|
||||
return pathJoin([location.pathname].concat(filepath.split("/").map(v => encodeURIComponent(v))))
|
||||
},
|
||||
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.name) + 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 = this.getEncodePath(f.name)
|
||||
// TODO: fix here tomorrow
|
||||
if (f.type == "file") {
|
||||
// check whether the file is video
|
||||
var videoExtensions = ['mp4', 'webm', 'ogg', 'mov', 'avi', 'mkv'];
|
||||
var fileExtension = getExtention(f.name).toLowerCase();
|
||||
if (videoExtensions.includes(fileExtension)) {
|
||||
window.location.href = '/-/video-player' + reqPath;
|
||||
} else {
|
||||
window.location.href = reqPath;
|
||||
}
|
||||
e.preventDefault()
|
||||
return;
|
||||
}
|
||||
loadFileOrDir(reqPath);
|
||||
e.preventDefault()
|
||||
},
|
||||
changePath: function (reqPath, e) {
|
||||
loadFileOrDir(reqPath);
|
||||
e.preventDefault()
|
||||
},
|
||||
showInfo: function (f) {
|
||||
console.log(f);
|
||||
$.ajax({
|
||||
url: this.getEncodePath(f.name),
|
||||
data: {
|
||||
op: "info",
|
||||
},
|
||||
method: "GET",
|
||||
success: function (res) {
|
||||
$("#file-info-title").text(f.name);
|
||||
$("#file-info-content").text(JSON.stringify(res, null, 4));
|
||||
$("#file-info-modal").modal("show");
|
||||
// console.log(JSON.stringify(res, null, 4));
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
showErrorMessage(jqXHR)
|
||||
}
|
||||
})
|
||||
},
|
||||
makeDirectory: function () {
|
||||
var name = window.prompt("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: this.getEncodePath(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 " + f.name + " ?")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
$.ajax({
|
||||
url: this.getEncodePath(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;
|
||||
}
|
||||
49
assets/video-player.html
Normal file
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Video Player - [[.FileName]]</title>
|
||||
<style>
|
||||
body, html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
background-color: #000;
|
||||
}
|
||||
.video-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
video {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
h1 {
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="video-container">
|
||||
<video id="videoPlayer" controls autoplay>
|
||||
<source src="[[.VideoURL]]" type="video/[[.Extension]]">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var video = document.getElementById('videoPlayer');
|
||||
video.focus();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
3
build.sh
@@ -23,13 +23,10 @@ fi
|
||||
build() {
|
||||
echo "$1 $2 ..."
|
||||
GOOS=$1 GOARCH=$2 go build \
|
||||
-tags bindata \
|
||||
-ldflags "$LDFLAGS" \
|
||||
-o dist/gohttpserver-${3:-""}
|
||||
}
|
||||
|
||||
go-bindata-assetfs -tags bindata res/...
|
||||
|
||||
build linux arm linux-arm
|
||||
build darwin amd64 mac-amd64
|
||||
build linux amd64 linux-amd64
|
||||
|
||||
15
docker/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM golang:1.16
|
||||
WORKDIR /app/gohttpserver
|
||||
ADD . /app/gohttpserver
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags '-X main.VERSION=docker' -o gohttpserver
|
||||
|
||||
FROM debian:stable
|
||||
WORKDIR /app
|
||||
RUN mkdir -p /app/public
|
||||
RUN apt-get update && apt-get install -y ca-certificates
|
||||
VOLUME /app/public
|
||||
ADD assets ./assets
|
||||
COPY --from=0 /app/gohttpserver/gohttpserver .
|
||||
EXPOSE 8000
|
||||
ENTRYPOINT [ "/app/gohttpserver", "--root=/app/public" ]
|
||||
CMD []
|
||||
19
docker/Dockerfile.alpine
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM golang:1.16.0-alpine as build
|
||||
|
||||
WORKDIR /app
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
RUN go mod download
|
||||
# Binary Compression
|
||||
RUN apk add upx --no-cache
|
||||
|
||||
# Build app and pack using upx
|
||||
ADD . /app
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags '-s -w -X main.VERSION=docker' -o gohttpserver && upx gohttpserver
|
||||
|
||||
FROM alpine:3
|
||||
WORKDIR /app
|
||||
ADD assets /usr/local/bin/assets
|
||||
COPY --from=build /app/gohttpserver /usr/local/bin/gohttpserver
|
||||
EXPOSE 8000
|
||||
ENTRYPOINT [ "/usr/local/bin/gohttpserver" ]
|
||||
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
|
||||
25
go.mod
Normal file
@@ -0,0 +1,25 @@
|
||||
module github.com/codeskyblue/gohttpserver
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/alecthomas/kingpin v2.2.6+incompatible
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect
|
||||
github.com/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/smartystreets/goconvey v1.6.4 // indirect
|
||||
github.com/stretchr/testify v1.3.0
|
||||
golang.org/x/net v0.23.0 // indirect
|
||||
golang.org/x/text v0.14.0
|
||||
howett.net/plist v0.0.0-20201203080718-1454fab16a06 // indirect
|
||||
)
|
||||
107
go.sum
Normal file
@@ -0,0 +1,107 @@
|
||||
github.com/alecthomas/kingpin v2.2.6+incompatible h1:5svnBTFgJjZvGKyYBtMB0+m5wvrbUHiqye8wRJMlnYI=
|
||||
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/codeskyblue/dockerignore v0.0.0-20151214070507-de82dee623d9 h1:c9axcChJwkLuSl9AvwTHi8jiBa6+VX4gGgERhABgv2E=
|
||||
github.com/codeskyblue/dockerignore v0.0.0-20151214070507-de82dee623d9/go.mod h1:XNZkUhPf+qgRnhY/ecS3B73ODJ2NXCzDMJHXM069IMg=
|
||||
github.com/codeskyblue/go-accesslog v0.0.0-20171215023101-6188d3bd9371 h1:dEBIvaVFaP2Uc9QA6J41qWxE5NfEnDWEBk+kWv5nK5k=
|
||||
github.com/codeskyblue/go-accesslog v0.0.0-20171215023101-6188d3bd9371/go.mod h1:sgXnVxxZ1u72GAzc9s1SzpuPMxBDKfTg6F2PvDrPSJU=
|
||||
github.com/codeskyblue/openid-go v0.0.0-20160923065855-0d30842b2fb4 h1:66lzN78lwccK+BPztRgBiWCYzhlerQEVOh2oeBksu5I=
|
||||
github.com/codeskyblue/openid-go v0.0.0-20160923065855-0d30842b2fb4/go.mod h1:K/hSCtAHvnE9aM+LsYgVmgzPNFuWFdx6i9t6/3jNrZQ=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fork2fix/go-plist v0.0.0-20181126021357-36960be5e636 h1:ESUdS2eb8LyDQfboYyFBwAL+rqYhnTZ15ntw8BLsd9g=
|
||||
github.com/fork2fix/go-plist v0.0.0-20181126021357-36960be5e636/go.mod h1:v6KRhgoO1QKamoeuZ7yHqZIP8p6j9k41Tb0jCyOEmr4=
|
||||
github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
|
||||
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
||||
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d h1:lBXNCxVENCipq4D1Is42JVOP4eQjlB8TQ6H69Yx5J9Q=
|
||||
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/handlers v1.4.0 h1:XulKRWSQK5uChr4pEgSE4Tc/OcmnU9GJuSwdog/tZsA=
|
||||
github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
||||
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
|
||||
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/shogo82148/androidbinary v0.0.0-20180627093851-01c4bfa8b3b5 h1:bXRaUWl3Afe3F9YR5NU1U3UB5zjCHlu4im5p3J/LUYk=
|
||||
github.com/shogo82148/androidbinary v0.0.0-20180627093851-01c4bfa8b3b5/go.mod h1:05AjXWPWLdTIl9+REKhSmTeoJ6Wz5e9ir0Q0NRxCIKo=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
howett.net/plist v0.0.0-20201203080718-1454fab16a06 h1:QDxUo/w2COstK1wIBYpzQlHX/NqaQTcf9jyz347nI58=
|
||||
howett.net/plist v0.0.0-20201203080718-1454fab16a06/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
|
||||
@@ -3,6 +3,9 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
@@ -13,35 +16,61 @@ 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 Directory struct {
|
||||
size map[string]int64
|
||||
mutex *sync.RWMutex
|
||||
}
|
||||
|
||||
type HTTPStaticServer struct {
|
||||
Root string
|
||||
Upload bool
|
||||
Delete bool
|
||||
Title string
|
||||
Theme string
|
||||
PlistProxy string
|
||||
GoogleTrackerId string
|
||||
Root string
|
||||
Prefix string
|
||||
Upload bool
|
||||
Delete bool
|
||||
Title string
|
||||
Theme string
|
||||
PlistProxy string
|
||||
GoogleTrackerID string
|
||||
AuthType string
|
||||
DeepPathMaxDepth int
|
||||
NoIndex bool
|
||||
|
||||
indexes []IndexFileItem
|
||||
m *mux.Router
|
||||
bufPool sync.Pool // use sync.Pool caching buf to reduce gc ratio
|
||||
}
|
||||
|
||||
func NewHTTPStaticServer(root string) *HTTPStaticServer {
|
||||
if root == "" {
|
||||
root = "./"
|
||||
}
|
||||
root = filepath.ToSlash(root)
|
||||
func NewHTTPStaticServer(root string, noIndex bool) *HTTPStaticServer {
|
||||
// if root == "" {
|
||||
// root = "./"
|
||||
// }
|
||||
// root = filepath.ToSlash(root)
|
||||
root = filepath.ToSlash(filepath.Clean(root))
|
||||
if !strings.HasSuffix(root, "/") {
|
||||
root = root + "/"
|
||||
}
|
||||
@@ -51,31 +80,33 @@ func NewHTTPStaticServer(root string) *HTTPStaticServer {
|
||||
Root: root,
|
||||
Theme: "black",
|
||||
m: m,
|
||||
bufPool: sync.Pool{
|
||||
New: func() interface{} { return make([]byte, 32*1024) },
|
||||
},
|
||||
NoIndex: noIndex,
|
||||
}
|
||||
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
for {
|
||||
startTime := time.Now()
|
||||
log.Println("Started making search index")
|
||||
s.makeIndex()
|
||||
log.Printf("Completed search index in %v", time.Since(startTime))
|
||||
//time.Sleep(time.Second * 1)
|
||||
time.Sleep(time.Minute * 10)
|
||||
}
|
||||
}()
|
||||
if !noIndex {
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
for {
|
||||
startTime := time.Now()
|
||||
log.Println("Started making search index")
|
||||
s.makeIndex()
|
||||
log.Printf("Completed search index in %v", time.Since(startTime))
|
||||
//time.Sleep(time.Second * 1)
|
||||
time.Sleep(time.Minute * 10)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
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("/-/video-player/{path:.*}", s.hVideoPlayer)
|
||||
|
||||
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
|
||||
}
|
||||
@@ -84,91 +115,249 @@ func (s *HTTPStaticServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s.m.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Return real path with Seperator(/)
|
||||
func (s *HTTPStaticServer) getRealPath(r *http.Request) string {
|
||||
path := mux.Vars(r)["path"]
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
path = filepath.Clean(path) // prevent .. for safe issues
|
||||
relativePath, err := filepath.Rel(s.Prefix, path)
|
||||
if err != nil {
|
||||
relativePath = path
|
||||
}
|
||||
realPath := filepath.Join(s.Root, relativePath)
|
||||
return filepath.ToSlash(realPath)
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) hIndex(w http.ResponseWriter, r *http.Request) {
|
||||
path := mux.Vars(r)["path"]
|
||||
relPath := filepath.Join(s.Root, path)
|
||||
realPath := s.getRealPath(r)
|
||||
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, realPath)
|
||||
if r.FormValue("raw") == "false" || isDir(realPath) {
|
||||
if r.Method == "HEAD" {
|
||||
return
|
||||
}
|
||||
renderHTML(w, "assets/index.html", s)
|
||||
} else {
|
||||
if filepath.Base(path) == YAMLCONF {
|
||||
auth := s.readAccessConf(realPath)
|
||||
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)))
|
||||
}
|
||||
http.ServeFile(w, r, relPath)
|
||||
http.ServeFile(w, r, realPath)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) hStatus(w http.ResponseWriter, r *http.Request) {
|
||||
data, _ := json.MarshalIndent(s, "", " ")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) hDelete(w http.ResponseWriter, req *http.Request) {
|
||||
// only can delete file now
|
||||
path := mux.Vars(req)["path"]
|
||||
auth := s.readAccessConf(path)
|
||||
log.Printf("%#v", auth)
|
||||
if !auth.Delete {
|
||||
realPath := s.getRealPath(req)
|
||||
// path = filepath.Clean(path) // for safe reason, prevent path contain ..
|
||||
auth := s.readAccessConf(realPath)
|
||||
if !auth.canDelete(req) {
|
||||
http.Error(w, "Delete forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
err := os.Remove(filepath.Join(s.Root, path))
|
||||
|
||||
// TODO: path safe check
|
||||
err := os.RemoveAll(realPath)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
pathErr, ok := err.(*os.PathError)
|
||||
if ok {
|
||||
http.Error(w, pathErr.Op+" "+path+": "+pathErr.Err.Error(), 500)
|
||||
} else {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
return
|
||||
}
|
||||
w.Write([]byte("Success"))
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) hUpload(w http.ResponseWriter, req *http.Request) {
|
||||
path := mux.Vars(req)["path"]
|
||||
func (s *HTTPStaticServer) hUploadOrMkdir(w http.ResponseWriter, req *http.Request) {
|
||||
dirpath := s.getRealPath(req)
|
||||
|
||||
// check auth
|
||||
auth := s.readAccessConf(path)
|
||||
if !auth.Upload {
|
||||
auth := s.readAccessConf(dirpath)
|
||||
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 := s.getRealPath(r)
|
||||
|
||||
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) {
|
||||
path := mux.Vars(r)["path"]
|
||||
CompressToZip(w, filepath.Join(s.Root, path))
|
||||
CompressToZip(w, s.getRealPath(r))
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) hUnzip(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -185,13 +374,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,
|
||||
}
|
||||
@@ -204,7 +389,7 @@ func (s *HTTPStaticServer) hPlist(w http.ResponseWriter, r *http.Request) {
|
||||
path = path[0:len(path)-6] + ".ipa"
|
||||
}
|
||||
|
||||
relPath := filepath.Join(s.Root, path)
|
||||
relPath := s.getRealPath(r)
|
||||
plinfo, err := parseIPA(relPath)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
@@ -230,9 +415,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 +427,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, "assets/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) {
|
||||
@@ -282,27 +469,115 @@ func (s *HTTPStaticServer) genPlistLink(httpPlistLink string) (plistUrl string,
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) hFileOrDirectory(w http.ResponseWriter, r *http.Request) {
|
||||
path := mux.Vars(r)["path"]
|
||||
http.ServeFile(w, r, filepath.Join(s.Root, path))
|
||||
http.ServeFile(w, r, s.getRealPath(r))
|
||||
}
|
||||
|
||||
type 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)
|
||||
realPath := s.getRealPath(r)
|
||||
search := r.FormValue("search")
|
||||
auth := s.readAccessConf(realPath)
|
||||
auth.Upload = auth.canUpload(r)
|
||||
auth.Delete = auth.canDelete(r)
|
||||
maxDepth := s.DeepPathMaxDepth
|
||||
|
||||
// path string -> info os.FileInfo
|
||||
fileInfoMap := make(map[string]os.FileInfo, 0)
|
||||
@@ -318,7 +593,7 @@ func (s *HTTPStaticServer) hJSONList(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
infos, err := ioutil.ReadDir(localPath)
|
||||
infos, err := ioutil.ReadDir(realPath)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
@@ -329,9 +604,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,
|
||||
@@ -344,35 +622,41 @@ func (s *HTTPStaticServer) hJSONList(w http.ResponseWriter, r *http.Request) {
|
||||
lr.Name = filepath.ToSlash(name) // fix for windows
|
||||
}
|
||||
if info.IsDir() {
|
||||
name := deepPath(localPath, info.Name())
|
||||
name := deepPath(realPath, info.Name(), maxDepth)
|
||||
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 dirInfoSize = Directory{size: make(map[string]int64), mutex: &sync.RWMutex{}}
|
||||
|
||||
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
|
||||
@@ -381,6 +665,28 @@ func (s *HTTPStaticServer) makeIndex() error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) historyDirSize(dir string) int64 {
|
||||
dirInfoSize.mutex.RLock()
|
||||
size, ok := dirInfoSize.size[dir]
|
||||
dirInfoSize.mutex.RUnlock()
|
||||
|
||||
if ok {
|
||||
return size
|
||||
}
|
||||
|
||||
for _, fitem := range s.indexes {
|
||||
if filepath.HasPrefix(fitem.Path, dir) {
|
||||
size += fitem.Info.Size()
|
||||
}
|
||||
}
|
||||
|
||||
dirInfoSize.mutex.Lock()
|
||||
dirInfoSize.size[dir] = size
|
||||
dirInfoSize.mutex.Unlock()
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) findIndex(text string) []IndexFileItem {
|
||||
ret := make([]IndexFileItem, 0)
|
||||
for _, item := range s.indexes {
|
||||
@@ -410,16 +716,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()
|
||||
relPath := filepath.Join(s.Root, requestPath)
|
||||
if isFile(relPath) {
|
||||
relPath = filepath.Dir(relPath)
|
||||
func (s *HTTPStaticServer) readAccessConf(realPath string) (ac AccessConf) {
|
||||
relativePath, err := filepath.Rel(s.Root, realPath)
|
||||
if err != nil || relativePath == "." || relativePath == "" { // actually relativePath is always "." if root == realPath
|
||||
ac = s.defaultAccessConf()
|
||||
realPath = s.Root
|
||||
} else {
|
||||
parentPath := filepath.Dir(realPath)
|
||||
ac = s.readAccessConf(parentPath)
|
||||
}
|
||||
cfgFile := filepath.Join(relPath, ".ghs.yml")
|
||||
if isFile(realPath) {
|
||||
realPath = filepath.Dir(realPath)
|
||||
}
|
||||
cfgFile := filepath.Join(realPath, YAMLCONF)
|
||||
data, err := ioutil.ReadFile(cfgFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
@@ -434,11 +747,9 @@ func (s *HTTPStaticServer) readAccessConf(requestPath string) (ac AccessConf) {
|
||||
return
|
||||
}
|
||||
|
||||
func deepPath(basedir, name string) string {
|
||||
isDir := true
|
||||
func deepPath(basedir, name string, maxDepth int) string {
|
||||
// loop max 5, incase of for loop not finished
|
||||
maxDepth := 5
|
||||
for depth := 0; depth <= maxDepth && isDir; depth += 1 {
|
||||
for depth := 0; depth <= maxDepth; depth += 1 {
|
||||
finfos, err := ioutil.ReadDir(filepath.Join(basedir, name))
|
||||
if err != nil || len(finfos) != 1 {
|
||||
break
|
||||
@@ -452,7 +763,82 @@ func deepPath(basedir, name string) string {
|
||||
return name
|
||||
}
|
||||
|
||||
func isFile(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
return err == nil && info.Mode().IsRegular()
|
||||
func 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 renderHTML(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 checkFilename(name string) error {
|
||||
if strings.ContainsAny(name, "\\/:*<>|") {
|
||||
return errors.New("Name should not contains \\/:*<>|")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) hVideoPlayer(w http.ResponseWriter, r *http.Request) {
|
||||
path := mux.Vars(r)["path"]
|
||||
realPath := s.getRealPath(r)
|
||||
extension := strings.ToLower(strings.TrimPrefix(filepath.Ext(path), "."))
|
||||
|
||||
if _, err := os.Stat(realPath); os.IsNotExist(err) {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
fileName := filepath.Base(path)
|
||||
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
videoURL := fmt.Sprintf("%s://%s/%s", scheme, r.Host, path)
|
||||
|
||||
renderHTML(w, "assets/video-player.html", map[string]interface{}{
|
||||
"FileName": fileName,
|
||||
"VideoURL": videoURL,
|
||||
"Extension": extension,
|
||||
})
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
172
main.go
@@ -2,53 +2,69 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"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/gorilla/mux"
|
||||
)
|
||||
|
||||
type Configure struct {
|
||||
Conf *os.File `yaml:"-"`
|
||||
Addr string `yaml:"addr"`
|
||||
Port int `yaml:"port"`
|
||||
Root string `yaml:"root"`
|
||||
HttpAuth string `yaml:"httpauth"`
|
||||
Prefix string `yaml:"prefix"`
|
||||
HTTPAuth string `yaml:"httpauth"`
|
||||
Cert string `yaml:"cert"`
|
||||
Key string `yaml:"key"`
|
||||
Cors bool `yaml:"cors"`
|
||||
Theme string `yaml:"theme"`
|
||||
XHeaders bool `yaml:"xheaders"`
|
||||
Upload bool `yaml:"upload"`
|
||||
Delete bool `yaml:"delete"`
|
||||
PlistProxy string `yaml:"plistproxy"`
|
||||
Title string `yaml:"title"`
|
||||
Debug bool `yaml:"debug"`
|
||||
GoogleTrackerId string `yaml:"google-tracker-id"`
|
||||
GoogleTrackerID string `yaml:"google-tracker-id"`
|
||||
Auth struct {
|
||||
Type string `yaml:"type"` // openid|http|github
|
||||
OpenID string `yaml:"openid"`
|
||||
HTTP []string `yaml:"http"`
|
||||
ID string `yaml:"id"` // for oauth2
|
||||
Secret string `yaml:"secret"` // for oauth2
|
||||
} `yaml:"auth"`
|
||||
DeepPathMaxDepth int `yaml:"deep-path-max-depth"`
|
||||
NoIndex bool `yaml:"no-index"`
|
||||
}
|
||||
|
||||
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 +95,38 @@ 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.GoogleTrackerID = "UA-81205425-2"
|
||||
gcfg.Title = "Go HTTP File Server"
|
||||
gcfg.DeepPathMaxDepth = 5
|
||||
gcfg.NoIndex = false
|
||||
|
||||
kingpin.HelpFlag.Short('h')
|
||||
kingpin.Version(versionMessage())
|
||||
kingpin.Flag("conf", "config file path, yaml format").FileVar(&gcfg.Conf)
|
||||
kingpin.Flag("root", "root directory, default ./").Short('r').StringVar(&gcfg.Root)
|
||||
kingpin.Flag("addr", "listen address, default :8000").Short('a').StringVar(&gcfg.Addr)
|
||||
kingpin.Flag("prefix", "url prefix, eg /foo").StringVar(&gcfg.Prefix)
|
||||
kingpin.Flag("port", "listen port, default 8000").IntVar(&gcfg.Port)
|
||||
kingpin.Flag("addr", "listen address, eg 127.0.0.1:8000").Short('a').StringVar(&gcfg.Addr)
|
||||
kingpin.Flag("cert", "tls cert.pem path").StringVar(&gcfg.Cert)
|
||||
kingpin.Flag("key", "tls key.pem path").StringVar(&gcfg.Key)
|
||||
kingpin.Flag("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)").StringsVar(&gcfg.Auth.HTTP)
|
||||
kingpin.Flag("auth-openid", "OpenID auth identity url").StringVar(&gcfg.Auth.OpenID)
|
||||
kingpin.Flag("theme", "web theme, one of <black|green>").StringVar(&gcfg.Theme)
|
||||
kingpin.Flag("upload", "enable upload support").BoolVar(&gcfg.Upload)
|
||||
kingpin.Flag("delete", "enable delete support").BoolVar(&gcfg.Delete)
|
||||
kingpin.Flag("xheaders", "used when behide nginx").BoolVar(&gcfg.XHeaders)
|
||||
kingpin.Flag("cors", "enable cross-site HTTP request").BoolVar(&gcfg.Cors)
|
||||
kingpin.Flag("debug", "enable debug mode").BoolVar(&gcfg.Debug)
|
||||
kingpin.Flag("plistproxy", "plist proxy when server is not https").Short('p').StringVar(&gcfg.PlistProxy)
|
||||
kingpin.Flag("title", "server title").StringVar(&gcfg.Title)
|
||||
kingpin.Flag("google-tracker-id", "set to empty to disable it").StringVar(&gcfg.GoogleTrackerId)
|
||||
kingpin.Flag("google-tracker-id", "set to empty to disable it").StringVar(&gcfg.GoogleTrackerID)
|
||||
kingpin.Flag("deep-path-max-depth", "set to -1 to not combine dirs").IntVar(&gcfg.DeepPathMaxDepth)
|
||||
kingpin.Flag("no-index", "disable indexing").BoolVar(&gcfg.NoIndex)
|
||||
|
||||
kingpin.Parse() // first parse conf
|
||||
|
||||
@@ -117,6 +143,52 @@ func parseFlags() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func fixPrefix(prefix string) string {
|
||||
prefix = regexp.MustCompile(`/*$`).ReplaceAllString(prefix, "")
|
||||
if !strings.HasPrefix(prefix, "/") {
|
||||
prefix = "/" + prefix
|
||||
}
|
||||
if prefix == "/" {
|
||||
prefix = ""
|
||||
}
|
||||
return prefix
|
||||
}
|
||||
|
||||
func cors(next http.Handler) http.Handler {
|
||||
// access control and CORS middleware
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "*")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "*")
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func multiBasicAuth(auths []string) func(http.Handler) http.Handler {
|
||||
userPassMap := make(map[string]string)
|
||||
for _, auth := range auths {
|
||||
userpass := strings.SplitN(auth, ":", 2)
|
||||
if len(userpass) == 2 {
|
||||
userPassMap[userpass[0]] = userpass[1]
|
||||
}
|
||||
}
|
||||
return httpauth.BasicAuth(httpauth.AuthOptions{
|
||||
Realm: "Restricted",
|
||||
AuthFunc: func(user, pass string, request *http.Request) bool {
|
||||
password, ok := userPassMap[user]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
givenPass := sha256.Sum256([]byte(pass))
|
||||
requiredPass := sha256.Sum256([]byte(password))
|
||||
return subtle.ConstantTimeCompare(givenPass[:], requiredPass[:]) == 1
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := parseFlags(); err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -125,12 +197,23 @@ func main() {
|
||||
data, _ := yaml.Marshal(gcfg)
|
||||
fmt.Printf("--- config ---\n%s\n", string(data))
|
||||
}
|
||||
log.SetFlags(log.Lshortfile | log.LstdFlags)
|
||||
|
||||
ss := NewHTTPStaticServer(gcfg.Root)
|
||||
// make sure prefix matches: ^/.*[^/]$
|
||||
gcfg.Prefix = fixPrefix(gcfg.Prefix)
|
||||
if gcfg.Prefix != "" {
|
||||
log.Printf("url prefix: %s", gcfg.Prefix)
|
||||
}
|
||||
|
||||
ss := NewHTTPStaticServer(gcfg.Root, gcfg.NoIndex)
|
||||
ss.Prefix = gcfg.Prefix
|
||||
ss.Theme = gcfg.Theme
|
||||
ss.Title = gcfg.Title
|
||||
ss.GoogleTrackerId = gcfg.GoogleTrackerId
|
||||
ss.GoogleTrackerID = gcfg.GoogleTrackerID
|
||||
ss.Upload = gcfg.Upload
|
||||
ss.Delete = gcfg.Delete
|
||||
ss.AuthType = gcfg.Auth.Type
|
||||
ss.DeepPathMaxDepth = gcfg.DeepPathMaxDepth
|
||||
|
||||
if gcfg.PlistProxy != "" {
|
||||
u, err := url.Parse(gcfg.PlistProxy)
|
||||
@@ -140,44 +223,73 @@ 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)
|
||||
switch gcfg.Auth.Type {
|
||||
case "http":
|
||||
hdlr = multiBasicAuth(gcfg.Auth.HTTP)(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)
|
||||
}
|
||||
hdlr = cors(hdlr)
|
||||
|
||||
if gcfg.XHeaders {
|
||||
hdlr = handlers.ProxyHeaders(hdlr)
|
||||
}
|
||||
|
||||
http.Handle("/", hdlr)
|
||||
http.HandleFunc("/-/sysinfo", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
mainRouter := mux.NewRouter()
|
||||
router := mainRouter
|
||||
if gcfg.Prefix != "" {
|
||||
router = mainRouter.PathPrefix(gcfg.Prefix).Subrouter()
|
||||
mainRouter.Handle(gcfg.Prefix, hdlr)
|
||||
mainRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, gcfg.Prefix, http.StatusTemporaryRedirect)
|
||||
})
|
||||
}
|
||||
|
||||
router.PathPrefix("/-/assets/").Handler(http.StripPrefix(gcfg.Prefix+"/-/", http.FileServer(Assets)))
|
||||
router.HandleFunc("/-/sysinfo", func(w http.ResponseWriter, r *http.Request) {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"version": VERSION,
|
||||
})
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(data)))
|
||||
w.Write(data)
|
||||
})
|
||||
router.PathPrefix("/").Handler(hdlr)
|
||||
|
||||
if gcfg.Addr == "" {
|
||||
gcfg.Addr = fmt.Sprintf(":%d", gcfg.Port)
|
||||
}
|
||||
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)
|
||||
|
||||
srv := &http.Server{
|
||||
Handler: mainRouter,
|
||||
Addr: gcfg.Addr,
|
||||
}
|
||||
|
||||
var err error
|
||||
if gcfg.Key != "" && gcfg.Cert != "" {
|
||||
err = http.ListenAndServeTLS(gcfg.Addr, gcfg.Cert, gcfg.Key, nil)
|
||||
err = srv.ListenAndServeTLS(gcfg.Cert, gcfg.Key)
|
||||
} else {
|
||||
err = http.ListenAndServe(gcfg.Addr, nil)
|
||||
err = srv.ListenAndServe()
|
||||
}
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
27
oauth2-proxy.go
Normal file
@@ -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
1
testdata/filetypes/script.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
document.write("Hello world!")
|
||||
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
74
utils.go
@@ -1,28 +1,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"path/filepath"
|
||||
"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 +56,43 @@ 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()
|
||||
}
|
||||
|
||||
// Convert path to normal paths
|
||||
func cleanPath(path string) string {
|
||||
return filepath.ToSlash(filepath.Clean(path))
|
||||
}
|
||||
|
||||
func isFile(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
return err == nil && info.Mode().IsRegular()
|
||||
}
|
||||
|
||||
func isDir(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
return err == nil && info.Mode().IsDir()
|
||||
}
|
||||
|
||||
@@ -25,3 +25,25 @@ func TestSublimeContains(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
orig string
|
||||
expect string
|
||||
}{
|
||||
// {"C:\\hello", "C:/hello"}, // Only works in windows
|
||||
{"", "."},
|
||||
{"//../foo", "/foo"},
|
||||
{"/../../", "/"},
|
||||
{"/hello/world/..", "/hello"},
|
||||
{"/..", "/"},
|
||||
{"/foo/..", "/"},
|
||||
{"/-/foo", "/-/foo"},
|
||||
}
|
||||
for _, v := range tests {
|
||||
res := cleanPath(v.orig)
|
||||
if res != v.expect {
|
||||
t.Fatalf("Clean path(%v) expect(%v) but got(%v)", v.orig, v.expect, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||