Compare commits

...

194 commits

Author SHA1 Message Date
Augusto Dwenger J. 627dea49c5 Add docker setup section to the README 2021-04-01 11:31:01 +02:00
Augusto Dwenger 777ff2376d Merge remote-tracking branch 'upstream/master'
Following features where merged:
 - Add support delete non-empty folder.
 - Add Cyan theme.
 - Sync status bar color with border-color on mobile platforms.

There are also some other changes but I am not able to understand them
from there commit message...
* upstream/master:
  update readme
  merge pull request
  Fix the assignment of sync buf
  update docker badge
  Optimize the performance of the copy phase during the upload process
  support delete non-empty folder, close #97
  update readme
  fix goreleaser again
  fix goreleaser again
  fix goreleaser
  fix upload large file >32M, close #98
  Update travis.yml
  Edit README.
  Add Cyan theme.
  Sync status bar color with border-color on mobile platforms.
  Use Go 1.14 & Update copyright string.
  fix: 修复问题
  fix: 修复问题
  fix: issue#49
2021-04-01 10:33:57 +02:00
codeskyblue 41981a3d8a update readme 2021-04-01 11:16:51 +08:00
codeskyblue 47789eec68 Merge branch 'aeoluswing-master' 2021-03-30 17:04:43 +08:00
codeskyblue 4c880193ce merge pull request 2021-03-30 17:04:23 +08:00
codeskyblue 86aeecfac2 Merge branch 'master' of https://github.com/aeoluswing/gohttpserver into aeoluswing-master 2021-03-30 16:47:34 +08:00
Augusto Dwenger J. 284d332424 Change github docker registry to selfhosted registry 2021-03-28 18:03:18 +02:00
Augusto Dwenger J. 2e58b052f9 Update build to use latest go version and create alpine based image 2021-03-28 18:01:39 +02:00
aeolus e8af97872c
Merge pull request #2 from aeoluswing/fix-uplimit-20210326
Fix the assignment of sync buf
2021-03-26 18:58:16 +08:00
aeoluswing d7590729f1 Fix the assignment of sync buf 2021-03-26 18:50:06 +08:00
codeskyblue f7063651c2 update docker badge 2021-03-26 15:57:47 +08:00
codeskyblue 42d1e077c9 merge theme 2021-03-26 15:41:28 +08:00
aeolus 1258ac759f
Merge pull request #1 from aeoluswing/fix-uplimit-20210326
Optimize the performance of the copy phase during the upload process
2021-03-26 14:18:29 +08:00
aeoluswing a60a9c4811 Optimize the performance of the copy phase during the upload process 2021-03-26 14:03:54 +08:00
codeskyblue b6200a88c0 support delete non-empty folder, close #97 2021-03-24 22:23:09 +08:00
codeskyblue 1618440f4f update readme 2021-03-24 15:02:18 +08:00
codeskyblue f676d25787 fix goreleaser again 2021-03-24 11:18:55 +08:00
codeskyblue e24ff056f7 fix goreleaser again 2021-03-24 11:09:37 +08:00
codeskyblue 833443dfaf fix goreleaser 2021-03-24 11:06:45 +08:00
codeskyblue b6f8563d18 Merge branch 'mahuiplus-master' 2021-03-24 10:38:52 +08:00
codeskyblue 20ed231c53 Merge branch 'master' of https://github.com/mahuiplus/gohttpserver into mahuiplus-master 2021-03-24 10:36:52 +08:00
codeskyblue b4470cf4bd fix upload large file >32M, close #98 2021-03-23 22:26:42 +08:00
Augusto Dwenger 83c202fde3 Remove automatic armhf image build 2020-12-05 16:13:49 +01:00
Augusto Dwenger 65f98b227a Merge branch 'update-builds' 2020-12-05 16:09:56 +01:00
Augusto Dwenger 74545dd8bc Add drone ci as a replacement for travis 2020-12-05 16:05:04 +01:00
Augusto Dwenger 400b503587 Update go module
The module name now points to the right repository and all dependencies
are updated.
2020-12-05 15:55:54 +01:00
Augusto Dwenger 3846857b38 Update docker image to version 1.15.6 2020-12-05 15:49:45 +01:00
Augusto Dwenger aa3eabc9e2 Remove old build and publish pipelines 2020-12-05 15:48:41 +01:00
Augusto Dwenger J. 6eb81b7579 Update the copyright notes and holders 2020-12-05 15:01:38 +01:00
Augusto Dwenger J. ecc6de50a1 Update README to have current fork information 2020-12-04 22:00:39 +01:00
Augusto Dwenger J. e58ae4bd33 Remove all google-analytic references 2020-12-04 21:44:03 +01:00
codeskyblue bd4e9250c0 fix docker build 2020-10-10 15:05:59 +08:00
codeskyblue 00940603ca change go version to go13 2020-10-10 13:49:51 +08:00
codeskyblue 621dc6c22f use move instead copy when handle big upload files 2020-10-09 20:05:23 +08:00
hathlife 5367b9ea94 Update travis.yml 2020-04-27 15:53:43 +08:00
hathlife 8e275e27be Edit README. 2020-04-27 15:40:54 +08:00
hathlife 56e184748f Add Cyan theme. 2020-04-27 15:40:54 +08:00
hathlife b40bf8101f Sync status bar color with border-color on mobile platforms. 2020-04-27 15:40:30 +08:00
hathlife 70e364c57b Use Go 1.14 & Update copyright string. 2020-03-25 21:21:36 +08:00
MaHui 98b09cb27f fix: 修复问题 2019-08-10 12:21:51 +08:00
MaHui aad95ae813 fix: 修复问题 2019-08-08 08:38:53 +08:00
MaHui 2bd7ec5cd0 fix: issue#49 2019-08-05 19:09:14 +08:00
shengxiang 85b2bd5dc4
Merge pull request #64 from chenwx36/master
better features
2019-03-02 21:56:55 +08:00
chenwenxiao fdec6aa9ac conflict resolve 2019-02-22 17:23:32 +08:00
chenwenxiao eaa82b8605 fix bug 2019-02-22 16:51:29 +08:00
chenwenxiao f4bdbeee73 fix bug 2019-02-22 16:26:55 +08:00
chenwenxiao 905412e91b fix bug 2019-02-22 16:24:34 +08:00
chenwenxiao 9318d19383 modify archive zip path & modify delete confirm prompt & modify delete fail message 2019-02-22 15:48:32 +08:00
chenwenxiao d62e765ae7 Merge remote-tracking branch 'origin/master' 2019-02-22 10:55:18 +08:00
chenwenxiao 097ff92f17 show path name where create folder 2019-02-22 10:54:58 +08:00
adolli fe28075b35
more about the oauth2 proxy mode 2019-02-22 10:54:37 +08:00
chenwenxiao 3339651d88 add delete path
add pathname check
2019-02-22 10:40:47 +08:00
codeskyblue c8d67edf2c format code and remove useless code 2019-02-21 20:23:29 +08:00
chenwenxiao 41ba422675 delete log 2019-02-21 15:48:20 +08:00
chenwenxiao f9f3142543 oauth2 2019-02-21 15:41:09 +08:00
codeskyblue 9442e00997 unzip ignore .ghs.yml 2019-02-15 17:51:32 +08:00
codeskyblue 1bbc103672 fix test 2019-02-15 17:41:35 +08:00
codeskyblue a16f689e99 add unzip support when upload 2019-02-15 17:28:46 +08:00
codeskyblue 6d5aae4dde unzip upload, not tested 2019-02-14 17:58:22 +08:00
codeskyblue 9f9d518689 fix click search button not search bug, close #50 2019-01-25 23:49:47 +08:00
codeskyblue 23441ea0c2 if ngnix is https, skip using plistproxy 2019-01-25 13:27:39 +08:00
hzsunshx d22065f7e0 add location.search 2019-01-25 09:49:32 +08:00
hzsunshx 8e3ddf6f3c fix #58 2019-01-25 09:25:53 +08:00
hzsunshx 0fdd7d5906 change /-/json/ to /?json=true 2019-01-24 18:08:15 +08:00
codeskyblue e0ced3f803 fix title bug, close #56 2019-01-09 08:44:11 +08:00
shengxiang 261a04d83f
Merge pull request #43 from FX-HAO/master
Add support for creating directory via HTTP requests (#35)
2018-12-15 10:47:11 +08:00
Fuxin Hao d0fd2cc458 Add support for creating directory via HTTP requests (#35) 2018-12-14 11:59:08 +08:00
shengxiang 7056ab85f8
Merge pull request #40 from gengjiawen/patch-2
Add docker badge
2018-12-02 18:36:14 +08:00
Jiawen Geng 84d280a9a4
Add docker layer 2018-12-01 19:14:33 +08:00
codeskyblue 21ef8634cf upgrade to go1.11 2018-11-26 10:32:22 +08:00
shengxiang 392f251bc2
Merge pull request #34 from adolli/patch-1
update README
2018-09-29 17:39:32 +08:00
adolli 727a1cdbfa
update README
fix English expressions
2018-09-28 11:29:11 +08:00
codeskyblue 5333393394 fix qrcode can not fix well with chinese 2018-09-26 21:04:58 +08:00
codeskyblue 6556afc08a support change upload with filename 2018-09-26 20:06:09 +08:00
codeskyblue c012da6e2e fix fix fix fix 2018-09-26 17:48:51 +08:00
codeskyblue 44fb4d2d09 fix fix fix 2018-09-26 17:15:18 +08:00
codeskyblue 64299ce9f9 fix and fix 2018-09-26 17:06:12 +08:00
codeskyblue 4f6226d83d update travis again 2018-09-26 16:49:39 +08:00
codeskyblue afe8cb7692 skip cleanup 2018-09-26 16:44:42 +08:00
codeskyblue a80617c85f last test 2018-09-26 16:40:05 +08:00
codeskyblue fac40bac1c add push manifest support 2018-09-26 16:25:02 +08:00
codeskyblue cdc5b318a0 add version info 2018-09-26 16:10:27 +08:00
codeskyblue b0403b60d9 fix docker usage 2018-09-26 13:57:53 +08:00
codeskyblue d7b886751b update travis for docker push 2018-09-26 13:52:24 +08:00
codeskyblue 22555ed424 support change to parent dir in search result 2018-09-26 10:08:43 +08:00
codeskyblue 1c60ccc233 set homebrew-tap 2018-09-25 18:51:02 +08:00
codeskyblue 3175093adf fix go version 2018-09-25 16:15:06 +08:00
codeskyblue 72a9609acb update travis 2018-09-25 16:12:11 +08:00
codeskyblue b2923eaa4a use cached template when use vfs mode 2018-09-25 15:59:38 +08:00
codeskyblue c3218a122f update doc to latest 2018-09-25 14:35:41 +08:00
codeskyblue 65bb5c6302 add comment 2018-09-18 13:38:41 +08:00
codeskyblue 0cacad260e support folder delete, prevent ghs.yml see 2018-09-18 13:35:10 +08:00
codeskyblue 40f02fba65 add ca-certificates for openid 2018-09-17 21:44:45 +08:00
codeskyblue e5bb87aaa1 add missing bootstrap 2018-09-17 21:05:15 +08:00
codeskyblue 80b84ad6b9 remove res 2018-09-17 21:01:50 +08:00
codeskyblue 395537a5d2 fix conflicts 2018-09-17 20:37:56 +08:00
codeskyblue dc03b85283 fix 2018-09-17 20:35:25 +08:00
codeskyblue 56aec4c702 fix with new dockerfile 2018-09-17 20:34:55 +08:00
codeskyblue 951128c735 fix with new dockerfile 2018-09-17 20:27:10 +08:00
codeskyblue 8e4187fe19 use vfsgen instead gobindata 2018-09-14 21:03:27 +08:00
Jiawen Geng 403a2ea84b Update Docker Usage 2017-12-27 18:54:38 +08:00
gengjiawen 7f4444b8a3 update README 2017-12-27 08:02:40 +08:00
Fuxin Hao bd7666b3e6 Use multi-stage builds 2017-12-27 08:02:40 +08:00
Fuxin Hao 876b8cffc4 Correct dependencies 2017-12-27 08:02:40 +08:00
gengjiawen 6bfa914a51 fix 2017-12-27 08:02:40 +08:00
gengjiawen 24ef16e80e initial support 2017-12-27 08:02:40 +08:00
shengxiang cc6b6987f8 update access-log 2017-12-15 10:07:56 +08:00
codeskyblue 1b217ca440 remove 1g limit 2017-09-11 13:40:05 +08:00
codeskyblue 7587b8067a add travis and goreleaser 2017-09-11 12:59:02 +08:00
codeskyblue 2b7fb84a5e update doc 2017-09-11 12:56:04 +08:00
codeskyblue 963f2ddb32 update doc 2017-08-31 23:35:42 +08:00
codeskyblue 3ff2620ba5 return json response when upload 2017-06-14 23:06:24 +08:00
Wei 22c83d8f2d Reword README
Fixed a few English language problem.
2017-06-08 23:47:45 +08:00
codeskyblue cab407a098 sync to new androidbinary lib 2017-06-01 17:00:23 +08:00
codeskyblue 4d21cd5dd8 fix js error 2017-05-22 23:00:40 +08:00
codeskyblue 26cb73de1c remove useless console.log 2017-05-22 22:41:41 +08:00
codeskyblue caef8c0fbd make qrcode page large 2017-05-22 19:11:34 +08:00
codeskyblue ea3d3fdcef wechat is support appstore link now, no need to show wechat redirect page anymore 2017-05-22 17:06:41 +08:00
codeskyblue 4d2c5587d6 remove temp file associated with form after uploaded 2017-05-19 16:04:24 +08:00
codeskyblue 6b3f0f0ae6 quick hot fix androidbinary panic bug 2017-05-17 14:59:32 +08:00
codeskyblue 28e9a2c0ca fix little bug 2017-05-17 10:46:35 +08:00
codeskyblue e5677bec86 close #18 support show folder size 2017-05-17 10:28:00 +08:00
codeskyblue a1b16115df update api 2017-05-16 21:41:41 +08:00
codeskyblue 5439149b09 update vendor 2017-05-16 21:34:30 +08:00
codeskyblue 332276f6db disable auto recover 2017-05-11 17:07:05 +08:00
codeskyblue 66c45e1c6e add remove all upload button 2017-05-11 16:50:20 +08:00
codeskyblue 68654f8bd3 add some log 2017-05-11 16:47:17 +08:00
codeskyblue a95e38802d prevent out of memory 2017-05-11 16:20:37 +08:00
codeskyblue e063187656 add missing package 2017-05-09 17:17:53 +08:00
codeskyblue 892546343a fix parse apk panic 2017-05-09 17:10:58 +08:00
codeskyblue dc3b5695d4 remove 1.5 test 2017-05-09 16:42:17 +08:00
codeskyblue f8ee0c0459 add api -/apk/info/:path-of-apk 2017-05-09 16:38:29 +08:00
codeskyblue 947bbe9efa fix #16 2017-02-14 13:10:31 +08:00
codeskyblue 9af2b5b5f6 fix #8 2016-12-22 14:42:24 +08:00
codeskyblue 7e1387a787 fix display login logic. 2016-12-22 12:05:34 +08:00
codeskyblue ff1f38c494 add openid support2 2016-12-22 11:46:29 +08:00
codeskyblue de1d2ba243 Merge branch 'master' of https://github.com/codeskyblue/gohttpserver 2016-12-21 15:47:12 +08:00
codeskyblue 2ab2c553b6 add accessTables support 2016-12-21 15:47:03 +08:00
codeskyblue 6889309af7 format index.tmpl.html code 2016-12-21 15:15:17 +08:00
codeskyblue 61758ec3ce add HEAD support, close #10 2016-09-21 20:20:13 +08:00
codeskyblue 7a1831c6ec fix breadcrumb when path is /somedir/, got different feeling of /somedir 2016-08-26 13:34:08 +08:00
codeskyblue 31cafdfd16 fix make index error 2016-08-24 11:26:10 +08:00
codeskyblue 859574f1e0 fix search path error when root not . 2016-08-12 11:02:54 +08:00
codeskyblue b4efbd1b6b remove some useless code 2016-08-06 22:25:08 +08:00
codeskyblue a7fc7054c5 format size in browser 2016-08-05 22:15:57 +08:00
codeskyblue 9dd2d6da30 add relevent link 2016-08-05 17:36:13 +08:00
codeskyblue ad85705599 use fontawesome copy icon 2016-08-05 17:23:13 +08:00
codeskyblue b94703f6b9 fix sort 2016-08-05 17:13:29 +08:00
codeskyblue acad061682 sort by time desc 2016-08-05 16:53:44 +08:00
codeskyblue d89a841bfb add clipboard support 2016-08-05 13:42:52 +08:00
codeskyblue b6a2908b68 load js when load finished 2016-08-05 11:22:24 +08:00
codeskyblue 37b71cb7bf add history back button 2016-08-05 11:13:25 +08:00
codeskyblue 644cbbcfd6 close preview now 2016-08-04 20:02:12 +08:00
codeskyblue f9b2518734 load preview file 2016-08-04 19:59:00 +08:00
codeskyblue 479db243a5 fix when EX_LDFLAGS not null 2016-08-04 09:46:24 +08:00
codeskyblue 22c048eb3a update icon 2016-08-04 09:44:37 +08:00
codeskyblue 8a195449c8 add confirm for delete, not finish yet 2016-08-04 07:53:39 +08:00
codeskyblue 35f8ec889a update green style css 2016-08-03 23:02:45 +08:00
codeskyblue 9d65aad7d7 close #3 2016-08-03 22:57:45 +08:00
codeskyblue acf4be706b fix sort error 2016-08-03 15:48:51 +08:00
codeskyblue 575884440d support deletable 2016-08-03 14:54:26 +08:00
codeskyblue c9237c8627 fix fix 2016-08-03 10:18:54 +08:00
codeskyblue bb4ac43d67 fix google analytics 2016-08-03 10:12:59 +08:00
codeskyblue 7fd04ea15d add scroll up 2016-08-03 09:41:38 +08:00
codeskyblue b444194496 fix root and addr default error 2016-08-02 16:06:44 +08:00
codeskyblue 07e001fc4d add conf support, thanks to <http://git.oschina.net/sparon> 2016-08-02 16:01:15 +08:00
codeskyblue de1deaac3d can open or close formNow time now 2016-08-02 14:43:15 +08:00
codeskyblue fa7b583962 update deps 2016-08-02 14:02:43 +08:00
codeskyblue 17cd98c75e add modtime, for download when click download 2016-08-02 14:02:23 +08:00
codeskyblue 43cb263853 support .ghs.yml add google analytics 2016-08-02 13:34:02 +08:00
codeskyblue 2a229b80ab reload file list disable cache 2016-08-01 20:08:18 +08:00
codeskyblue 5641c9e8ff show qrcode for all file 2016-08-01 16:37:30 +08:00
codeskyblue 48ecbf43ab html also let support qrcode 2016-08-01 16:15:42 +08:00
codeskyblue d5fc4b6153 not let qrcode jump 2016-08-01 16:06:45 +08:00
codeskyblue 29a8557e98 fix js error 2016-08-01 15:59:09 +08:00
codeskyblue c957548d7b add ex_ldflags 2016-08-01 15:34:41 +08:00
codeskyblue a7c6d06479 short code 2016-08-01 15:07:13 +08:00
codeskyblue c8768d3c21 add css test type 2016-08-01 14:53:20 +08:00
codeskyblue cdda0a01a5 update index 2016-08-01 14:50:24 +08:00
codeskyblue 17a7f658c9 use heroku plistproxy as default proxy 2016-08-01 14:02:24 +08:00
codeskyblue 01a7c39709 add binary release 2016-08-01 11:19:50 +08:00
codeskyblue 7dba3a4c7d change --xproxy to --xheaders 2016-08-01 10:42:32 +08:00
codeskyblue 8944cc072e add plist proxy 2016-08-01 10:24:06 +08:00
codeskyblue 4f958f1c24 update search algorighm 2016-08-01 09:37:59 +08:00
codeskyblue 3fb6620065 Merge branch 'master' of https://github.com/codeskyblue/gohttpserver 2016-08-01 09:21:01 +08:00
codeskyblue a1aac2bedc fix on windows -r R: error but -r R:/ ok 2016-08-01 09:20:58 +08:00
codeskyblue 8cb2b90e66 update css 2016-07-31 22:53:22 +08:00
codeskyblue cf2366df85 fix multi index 2016-07-31 22:28:09 +08:00
codeskyblue ff1067f9f7 add making fs index support, to speed up search 2016-07-31 19:31:05 +08:00
codeskyblue d3a12f7c69 add execute for build.sh 2016-07-31 18:11:52 +08:00
codeskyblue 2f1b55892a add file search support 2016-07-31 11:47:02 +08:00
codeskyblue d7e808cd4f fix spell error 2016-07-31 10:33:45 +08:00
codeskyblue 1c3a1d28da add dev version 2016-07-31 10:22:55 +08:00
codeskyblue a1c1c0f987 add auto tag version support 2016-07-31 10:19:32 +08:00
191 changed files with 3858 additions and 18357 deletions

47
.drone.yml Normal file
View file

@ -0,0 +1,47 @@
kind: pipeline
type: docker
name: tests
steps:
- name: unit-test
image: golang
volumes:
- name: cache
path: /go
commands:
- go test -coverprofile=coverage.out -covermode=count ./...
- go tool cover -func=coverage.out | grep total
- name: race-test
image: golang
volumes:
- name: cache
path: /go
commands:
- go test -race ./...
volumes:
- name: cache
temp: {}
---
kind: pipeline
type: docker
name: docker-build
steps:
- name: docker
image: plugins/docker
settings:
registry: registry.hhhammer.de
username: ci
password:
from_secret: DOCKER_REGISTRY_KEY
repo: registry.hhhammer.de/gohttpserver
dockerfile: docker/Dockerfile
auto_tag: true
pull_image: true
depends_on:
- tests

View file

@ -2,11 +2,12 @@ desc: Auto generated by fswatch [gohttp-vue]
triggers:
- name: ""
pattens:
- '!.git/'
- '**/*.go'
- '**/*.tmpl.html'
env:
DEBUG: "1"
cmd: (killall gohttpserver; true) && go build && ./gohttpserver --upload&
cmd: go build && ./gohttpserver --upload --root testdata
shell: true
delay: 100ms
signal: KILL

2
.ghs.yml Normal file
View file

@ -0,0 +1,2 @@
upload: true
delete: false

5
.gitignore vendored
View file

@ -23,8 +23,11 @@ _testmain.go
*.test
*.prof
dist/
gohttpserver
bindata_assetfs.go
assets_vfsdata.go
*.un~
*.swp
dist/

View file

@ -1,9 +0,0 @@
language: go
go:
- 1.5
- 1.6
env:
global:
- GO15VENDOREXPERIMENT=1
script:
- go test -v

55
Godeps/Godeps.json generated
View file

@ -1,55 +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/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
View file

@ -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.

View file

@ -1,6 +1,7 @@
The MIT License (MIT)
Copyright (c) 2016 shengxiang
Copyright (c) 2020 Augusto Dwenger J.
Copyright (c) 2018 shengxiang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

265
README.md
View file

@ -1,20 +1,20 @@
# gohttpserver
[![Build Status](https://travis-ci.org/codeskyblue/gohttpserver.svg?branch=master)](https://travis-ci.org/codeskyblue/gohttpserver)
This is a fork from [codeskyblue/gohttpserver](https://github.com/codeskyblue/gohttpserver/) without google-analytics.
Make the best HTTP File Server. Better UI, upload support, apple&android install package qrcode generate.
- Goal: Make the best HTTP File Server.
- Features: Human-friendly UI, file uploading support, direct QR-code generation for Apple & Android install package.
[Demo site](https://gohttpserver.herokuapp.com/)
- 目标: 做最好的HTTP文件服务器
- 功能: 人性化的UI体验文件的上传支持安卓和苹果安装包的二维码直接生成。
## Notes
If using go1.5, ensure you set GO15VENDOREXPERIMENT=1
Upload size now limited to 1G
## Requirements
Tested with go-1.16
## Screenshots
![screen](screenshot.png)
![screen](testdata/filetypes/gohttpserver.gif)
## Features
1. [x] Support QRCode code generate
@ -22,7 +22,7 @@ Upload size now limited to 1G
1. [x] All assets package to Standalone binary
1. [x] Different file type different icon
1. [x] Support show or hide hidden files
1. [x] Upload support (for security reason, you need enabled it by option `--upload`)
1. [x] Upload support (auth by token or session)
1. [x] README.md preview
1. [x] HTTP Basic Auth
1. [x] Partial reload pages when directory change
@ -35,16 +35,25 @@ Upload size now limited to 1G
1. [ ] Offline download
1. [ ] Code file preview
1. [ ] Edit file support
1. [ ] Global file search
1. [x] Global file search
1. [x] Hidden work `download` and `qrcode` in small screen
1. [x] Theme select support
1. [x] OK to working behide Nginx
1. [ ] \.htaccess support
1. [x] \.ghs.yml support (like \.htaccess)
1. [ ] Calculate md5sum and sha
1. [ ] Folder upload
1. [ ] Support sort by size or modified time
1. [ ] Add version info into index page
1. [ ] Add api `/-/stat/some.(apk|ipa)` to get detail info
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
```
@ -52,68 +61,246 @@ go get -v github.com/codeskyblue/gohttpserver
cd $GOPATH/src/github.com/codeskyblue/gohttpserver
go build && ./gohttpserver
```
## Usage
Listen port 8000 on all interface, and enable upload
Listen on port 8000 of all interfaces, and enable file uploading.
```
./gohttpserver -r ./ --addr :8000 --upload
./gohttpserver -r ./ --port 8000 --upload
```
Use command `gohttpserver --help` to see more usage.
## Docker Usage
share current directory
```bash
docker run -it --rm -p 8000:8000 -v $PWD:/app/public --name gohttpserver registry.hhhammer.de/gohttpserver
```
Share current directory with http basic auth
```bash
docker run -it --rm -p 8000:8000 -v $PWD:/app/public --name gohttpserver \
registry.hhhammer.de/gohttpserver \
--auth-type http --auth-http username:password
```
Share current directory with openid auth. (Works only in netease company.)
```bash
docker run -it --rm -p 8000:8000 -v $PWD:/app/public --name gohttpserver \
registry.hhhammer.de/gohttpserver \
--auth-type openid
```
To build image yourself, please change the PWD to the root of this repo.
```bash
cd gohttpserver/
docker build -t registry.hhhhammer.de/gohttpserver -f docker/Dockerfile .
```
## Authentication options
- Enable basic http authentication
```sh
$ gohttpserver --auth-type http --auth-http username:password
```
- Use openid auth
```sh
$ gohttpserver --auth-type openid --auth-openid https://login.example-hostname.com/openid/
```
- Use oauth2-proxy with
```sh
$ gohttpserver --auth-type oauth2-proxy
```
You can configure to let a http reverse proxy handling authentication.
When using oauth2-proxy, the backend will use identification info from request headers `X-Auth-Request-Email` as userId and `X-Auth-Request-Fullname` as user's display name.
Please config your oauth2 reverse proxy yourself.
More about [oauth2-proxy](https://github.com/oauth2-proxy/oauth2-proxy).
All required headers list as following.
|header|value|
|---|---|
|X-Auth-Request-Email| userId |
|X-Auth-Request-Fullname| user's display name(urlencoded) |
|X-Auth-Request-User| user's nickname (mostly email prefix) |
- Enable upload
```sh
$ gohttpserver --upload
```
- Enable delete and Create folder
```sh
$ gohttpserver --delete
```
## Advanced usage
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
```
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 -
|-- foo
| |-- .ghs.yml
| `-- world.txt
`-- bar
`-- hello.txt
```
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.
This is used for server on which https is enabled. default use <https://plistproxy.herokuapp.com/plist>
```
./gohttpserver --plistproxy=https://someproxyhost.com/
```
Proxy web site should have ability, when request `https://proxyhost.com/www.github.com`
return the same page as request from `http://www.github.com`
Test if proxy works:
```sh
$ http POST https://someproxyhost.com/plist < app.plist
{
"key": "18f99211"
}
$ http GET https://someproxyhost.com/plist/18f99211
# show the app.plist content
```
If your ghs running behide nginx server and have https configed. plistproxy will be disabled automaticly.
### Upload with CURL
For example, upload a file named `foo.txt` to directory `somedir`
PS: max upload size limited to 1G (hard coded)
```sh
$ curl -F file=@foo.txt localhost:8000/somedir
{"destination":"somedir/foo.txt","success":true}
# upload with token
$ curl -F file=@foo.txt -F token=12312jlkjafs localhost:8000/somedir
{"destination":"somedir/foo.txt","success":true}
# upload and change filename
$ curl -F file=@foo.txt -F filename=hi.txt localhost:8000/somedir
{"destination":"somedir/hi.txt","success":true}
```
Upload zip file and unzip it (zip file will be delete when finished unzip)
```
$ curl -F file=@pkg.zip -F unzip=true localhost:8000/somedir
{"success": true}
```
Note: `\/:*<>|` are not allowed in filenames.
### Deploy with nginx
Recommended configuration, assume your gohttpserver listening on `127.0.0.1:8200`
```
server {
listen 80;
server_name your-domain-name.com;
location / {
proxy_pass http://127.0.0.1:8200; # here need to change
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 0; # disable upload limit
}
}
```
gohttpserver should started with `--xheaders` argument when behide nginx.
Refs: <http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size>
## FAQ
- [How to generate self signed certificate with openssl](http://stackoverflow.com/questions/10175812/how-to-create-a-self-signed-certificate-with-openssl)
### How the query is formated
The search query follows common format rules just like Google. Keywords are seperated with space(s), keywords with prefix `-` will be excluded in search results.
1. `hello world` means must contains `hello` and `world`
1. `hello -world` means must contains `hello` but not contains `world`
## Developer Guide
Depdencies are managed by godep
Depdencies are managed by [govendor](https://github.com/kardianos/govendor)
```sh
go get -v github.com/tools/godep
go get github.com/jteeuwen/go-bindata/...
go get github.com/elazarl/go-bindata-assetfs/...
```
1. Build develop version. **assets** directory must exists
Theme are all defined in [res/themes](res/themes) directory. Now only two, black and green.
```sh
go build
./gohttpserver
```
2. Build single binary release
## How to build single binary release
```sh
go-bindata-assetfs -tags bindata res/...
go build -tags bindata
```
```sh
go generate .
go build -tags vfs
```
Theme are defined in [assets/themes](assets/themes) directory. Now only two themes are available, "black" and "green".
That's all. ^_^
## Reference Web sites
* <https://vuejs.org.cn/>
* Core lib Vue <https://vuejs.org.cn/>
* Icon from <http://www.easyicon.net/558394-file_explorer_icon.html>
* <https://github.com/elazarl/go-bindata-assetfs>
* Code Highlight <https://craig.is/making/rainbows>
* Markdown-JS <https://github.com/showdownjs/showdown>
* <https://github.com/sindresorhus/github-markdown-css>
* Markdown Parser <https://github.com/showdownjs/showdown>
* 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**
* [vfsgen](https://github.com/shurcooL/vfsgen)
* [go-bindata-assetfs](https://github.com/elazarl/go-bindata-assetfs) Not using now
* <http://www.gorillatoolkit.org/pkg/handlers>
* <http://www.dropzonejs.com/>
## 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).

View file

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

View file

@ -0,0 +1,15 @@
/* Image style */
#scrollUp {
/*background-image: url("../imgs/top.png");*/
bottom: 5px;
right: 20px;
width: 38px;
/* Width of image */
height: 38px;
/* Height of image */
}
#scrollUp:after {
content: "Scroll to top";
}

View file

@ -6,7 +6,9 @@ body {
margin-bottom: 1em;
}
/* Not used */
div.dropzone {
display: block;
/*text-align: center;*/
@ -18,3 +20,15 @@ div.dropzone {
font-size: 20px;
position: relative;
}
.qrcode-title {
font-size: 0.8em;
}
.clearfix::after {
clear: both;
}
#qrcodeCanvas {
padding-right: 20px;
}

View file

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

Before

Width:  |  Height:  |  Size: 382 KiB

After

Width:  |  Height:  |  Size: 382 KiB

BIN
assets/imgs/top.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 B

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

301
assets/index.html Normal file
View file

@ -0,0 +1,301 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta name="theme-color" content="#000000">
<title>[[.Title]]</title>
<link rel="shortcut icon" type="image/png" href="/-/assets/favicon.png" />
<link rel="stylesheet" type="text/css" href="/-/assets/bootstrap-3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/-/assets/font-awesome-4.6.3/css/font-awesome.min.css">
<link rel="stylesheet" type="text/css" href="/-/assets/css/github-markdown.css">
<link rel="stylesheet" type="text/css" href="/-/assets/css/dropzone.css">
<link rel="stylesheet" type="text/css" href="/-/assets/css/scrollUp-image.css">
<link rel="stylesheet" type="text/css" href="/-/assets/css/style.css">
<link rel="stylesheet" type="text/css" href="/-/assets/themes/[[.Theme]].css">
</head>
<body id="app">
<nav class="navbar navbar-default">
<div class="container">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-2">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">[[.Title]]</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-2">
<ul class="nav navbar-nav">
<li class="hidden-xs">
<a href="javascript:void(0)" v-on:click='genQrcode()'>
View in Phone
<span class="glyphicon glyphicon-qrcode"></span>
</a>
</li>
[[if eq .AuthType "openid"]]
<template v-if="!user.email">
<a href="/-/login" class="btn btn-sm btn-default navbar-btn">
Sign in <span class="glyphicon glyphicon-user"></span>
</a>
</template>
<template v-else>
<a href="/-/logout" class="btn btn-sm btn-default navbar-btn">
<span v-text="user.name"></span>
<i class="fa fa-sign-out"></i>
</a>
</template>
[[end]]
[[if eq .AuthType "oauth2-proxy"]]
<template v-if="!user.email">
<a href="#" class="btn btn-sm btn-default navbar-btn">
Guest <span class="glyphicon glyphicon-user"></span>
</a>
</template>
<template v-else>
<a href="/-/logout" class="btn btn-sm btn-default navbar-btn">
<span v-text="user.name"></span>
<i class="fa fa-sign-out"></i>
</a>
</template>
[[end]]
</ul>
<form class="navbar-form navbar-right">
<div class="input-group">
<input type="text" name="search" class="form-control" placeholder="Search text" v-bind:value="search"
autofocus>
<span class="input-group-btn">
<button class="btn btn-default" type="submit">
<span class="glyphicon glyphicon-search"></span>
</button>
</span>
</div>
</form>
<ul id="nav-right-bar" class="nav navbar-nav navbar-right">
</ul>
</div>
</div>
</div>
</nav>
<div class="container">
<div class="col-md-12">
<ol class="breadcrumb">
<li>
<a v-on:click='changePath("/", $event)' href="/"><i class="fa fa-home"></i></a>
</li>
<li v-for="bc in breadcrumb.slice(0, breadcrumb.length-1)">
<a v-on:click='changePath(bc.path, $event)' href="{{bc.path}}">{{bc.name}}</a>
</li>
<li v-if="breadcrumb.length >= 1">
{{breadcrumb.slice(-1)[0].name}}
</li>
</ol>
<table class="table table-hover" v-if="!previewMode">
<thead>
<tr>
<td colspan=4>
<!-- <button class="btn btn-xs btn-default" v-on:click='toggleHidden()'>
Back <i class="fa" v-bind:class='showHidden ? "fa-eye" : "fa-eye-slash"'></i>
</button> -->
<div>
<button class="btn btn-xs btn-default" onclick="history.back()">
Back <i class="fa fa-arrow-left"></i>
</button>
<button class="btn btn-xs btn-default" v-on:click='toggleHidden()'>
Hidden <i class="fa" v-bind:class='showHidden ? "fa-eye" : "fa-eye-slash"'></i>
</button>
<button class="btn btn-xs btn-default" v-show="auth.upload" data-toggle="modal" data-target="#upload-modal">
Upload <i class="fa fa-upload"></i>
</button>
<button class="btn btn-xs btn-default" v-show="auth.delete" @click="makeDirectory">
New Folder <i class="fa fa-folder"></i>
</button>
</div>
</td>
</tr>
<tr>
<th>Name</th>
<th>Size</th>
<th class="hidden-xs">
<span style="cursor: pointer" v-on:click='mtimeTypeFromNow = !mtimeTypeFromNow'>ModTime</span>
</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="f in computedFiles">
<td>
<a v-on:click='clickFileOrDir(f, $event)' href="{{getEncodePath(f)}}">
<!-- ?raw=false -->
<i style="padding-right: 0.5em" class="fa" v-bind:class='genFileClass(f)'></i> {{f.name}}
</a>
<!-- for search -->
<button v-show="f.type == 'file' && f.name.indexOf('/') >= 0" class="btn btn-default btn-xs" @click="changeParentDirectory(f.path)">
<i class="fa fa-folder-open-o"></i>
</button>
</td>
<td><span v-if="f.type == 'dir'">~</span> {{f.size | formatBytes}}</td>
<td class="hidden-xs">{{formatTime(f.mtime)}}</td>
<td style="text-align: left">
<template v-if="f.type == 'dir'">
<a class="btn btn-default btn-xs" href="{{getEncodePath(f)}}/?op=archive">
<span class="hidden-xs">Archive</span> Zip
<span class="glyphicon glyphicon-download-alt"></span>
</a>
<button class="btn btn-default btn-xs" v-on:click="showInfo(f)">
<span class="glyphicon glyphicon-info-sign"></span>
</button>
<button class="btn btn-default btn-xs" v-if="auth.delete" v-on:click="deletePathConfirm(f, $event)">
<span style="color:#CC3300" class="glyphicon glyphicon-trash"></span>
</button>
</template>
<template v-if="f.type == 'file'">
<a class="btn btn-default btn-xs hidden-xs" href="{{genDownloadURL(f)}}">
<span class="hidden-xs">Download</span>
<span class="glyphicon glyphicon-download-alt"></span>
</a>
<button class="btn btn-default btn-xs bstooltip" data-trigger="manual" data-title="Copied!"
data-clipboard-text="{{genDownloadURL(f)}}">
<i class="fa fa-copy"></i>
</button>
<button class="btn btn-default btn-xs" v-on:click="showInfo(f)">
<span class="glyphicon glyphicon-info-sign"></span>
</button>
<button class="btn btn-default btn-xs hidden-xs" v-on:click="genQrcode(f.name)">
<span v-if="shouldHaveQrcode(f.name)">QRCode</span>
<span class="glyphicon glyphicon-qrcode"></span>
</button>
<a class="btn btn-default btn-xs visible-xs" v-if="shouldHaveQrcode(f.name)" href="{{genInstallURL(f.name)}}">
Install <i class="fa fa-cube"></i>
</a>
<button class="btn btn-default btn-xs" v-if="auth.delete" v-on:click="deletePathConfirm(f, $event)">
<span style="color:#CC3300" class="glyphicon glyphicon-trash"></span>
</button>
</template>
</td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-12" id="preview" v-if="preview.filename">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title" style="font-weight: normal">
<i class="fa" v-bind:class='genFileClass(previewFile)'></i> {{preview.filename}}
</h3>
</div>
<div class="panel-body">
<article class="markdown-body">{{{preview.contentHTML }}}
</article>
</div>
</div>
</div>
<div class="col-md-12" id="content">
<!-- Small qrcode modal -->
<div id="qrcode-modal" class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</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">&times;</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">&times;</span></button>
<h4 class="modal-title">
<span id="file-info-title"></span>
</h4>
</div>
<div class="modal-body">
<pre id="file-info-content"></pre>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-12">
<div id="footer" class="pull-right" style="margin: 2em 1em">
<a href="https://github.com/hamburghammer/gohttpserver">gohttpserver (ver:{{version}})</a>, fork from <a href="https://github.com/codeskyblue/gohttpserver">codeskyblue/gohttpserver</a>.
Lizensed under <a href="https://github.com/hamburghammer/gohttpserver/blob/master/LICENSE">MIT</a>.
</div>
</div>
</div>
<script src="/-/assets/js/jquery-3.1.0.min.js"></script>
<script src="/-/assets/js/jquery.qrcode.js"></script>
<script src="/-/assets/js/jquery.scrollUp.min.js"></script>
<script src="/-/assets/js/qrcode.js"></script>
<script src="/-/assets/js/vue-1.0.min.js"></script>
<script src="/-/assets/js/showdown-1.6.4.min.js"></script>
<script src="/-/assets/js/moment.min.js"></script>
<script src="/-/assets/js/dropzone.js"></script>
<script src="/-/assets/js/underscore-min.js"></script>
<script src="/-/assets/js/clipboard-1.5.12.min.js"></script>
<script src="/-/assets/bootstrap-3.3.5/js/bootstrap.min.js"></script>
<script src='/-/assets/[["js/index.js" | urlhash ]]'></script>
<!-- <script src="/-/assets/js/index.js"></script> -->
<!--Sync status bar color with border-color on mobile platforms.-->
<script>
var META = document.getElementsByTagName("meta");
META[2]["content"]=$('.navbar').css('border-color');
</script>
[[if .GoogleTrackerID ]]
<script>
(function (i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject'] = r;
i[r] = i[r] || function () {
(i[r].q = i[r].q || []).push(arguments)
}, i[r].l = 1 * new Date();
a = s.createElement(o),
m = s.getElementsByTagName(o)[0];
a.async = 1;
a.src = g;
m.parentNode.insertBefore(a, m)
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
ga('create', '[[.GoogleTrackerID]]', 'auto');
ga('send', 'pageview');
</script> [[ end ]]
</body>
</html>

71
assets/ipa-install.html Normal file
View 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

File diff suppressed because one or more lines are too long

454
assets/js/index.js Normal file
View file

@ -0,0 +1,454 @@
jQuery('#qrcodeCanvas').qrcode({
text: "http://jetienne.com/"
});
Dropzone.autoDiscover = false;
function getExtention(fname) {
return fname.slice((fname.lastIndexOf(".") - 1 >>> 0) + 2);
}
function pathJoin(parts, sep) {
var separator = sep || '/';
var replace = new RegExp(separator + '{1,}', 'g');
return parts.join(separator).replace(replace, separator);
}
function getQueryString(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
var r = decodeURI(window.location.search).substr(1).match(reg);
if (r != null) return r[2].replace(/\+/g, ' ');
return null;
}
function checkPathNameLegal(name) {
var reg = new RegExp("[\\/:*<>|]");
var r = name.match(reg)
return r == null;
}
function showErrorMessage(jqXHR) {
let errMsg = jqXHR.getResponseHeader("x-auth-authentication-message")
if (errMsg == null) {
errMsg = jqXHR.responseText
}
alert(String(jqXHR.status).concat(":", errMsg));
console.error(errMsg)
}
var vm = new Vue({
el: "#app",
data: {
user: {
email: "",
name: "",
},
location: window.location,
breadcrumb: [],
showHidden: false,
previewMode: false,
preview: {
filename: '',
filetype: '',
filesize: 0,
contentHTML: '',
},
version: "loading",
mtimeTypeFromNow: false, // or fromNow
auth: {},
search: getQueryString("search"),
files: [{
name: "loading ...",
path: "",
size: "...",
type: "dir",
}],
myDropzone: null,
},
computed: {
computedFiles: function () {
var that = this;
that.preview.filename = null;
var files = this.files.filter(function (f) {
if (f.name == 'README.md') {
that.preview.filename = f.name;
}
if (!that.showHidden && f.name.slice(0, 1) === '.') {
return false;
}
return true;
});
// console.log(this.previewFile)
if (this.preview.filename) {
var name = this.preview.filename; // For now only README.md
console.log(pathJoin([location.pathname, 'README.md']))
$.ajax({
url: pathJoin([location.pathname, 'README.md']),
method: 'GET',
success: function (res) {
var converter = new showdown.Converter({
tables: true,
omitExtraWLInCodeBlocks: true,
parseImgDimensions: true,
simplifiedAutoLink: true,
literalMidWordUnderscores: true,
tasklists: true,
ghCodeBlocks: true,
smoothLivePreview: true,
simplifiedAutoLink: true,
strikethrough: true,
});
var html = converter.makeHtml(res);
that.preview.contentHTML = html;
},
error: function (err) {
console.log(err)
}
})
}
return files;
},
},
created: function () {
$.ajax({
url: "/-/user",
method: "get",
dataType: "json",
success: function (ret) {
if (ret) {
this.user.email = ret.email;
this.user.name = ret.name;
}
}.bind(this)
})
this.myDropzone = new Dropzone("#upload-form", {
paramName: "file",
maxFilesize: 10240,
addRemoveLinks: true,
init: function () {
this.on("uploadprogress", function (file, progress) {
// console.log("File progress", progress);
});
this.on("complete", function (file) {
console.log("reload file list")
loadFileList()
})
}
});
},
methods: {
getEncodePath: function (f) {
return pathJoin([location.pathname, encodeURIComponent(f.name)]);
},
formatTime: function (timestamp) {
var m = moment(timestamp);
if (this.mtimeTypeFromNow) {
return m.fromNow();
}
return m.format('YYYY-MM-DD HH:mm:ss');
},
toggleHidden: function () {
this.showHidden = !this.showHidden;
},
removeAllUploads: function () {
this.myDropzone.removeAllFiles();
},
parentDirectory: function (path) {
return path.replace('\\', '/').split('/').slice(0, -1).join('/')
},
changeParentDirectory: function (path) {
var parentDir = this.parentDirectory(path);
loadFileOrDir(parentDir);
},
genInstallURL: function (name, noEncode) {
var parts = [location.host];
var pathname = decodeURI(location.pathname);
if (!name) {
parts.push(pathname);
} else if (getExtention(name) == "ipa") {
parts.push("/-/ipa/link", pathname, encodeURIComponent(name));
} else {
parts.push(pathname, name);
}
var urlPath = location.protocol + "//" + pathJoin(parts);
return noEncode ? urlPath : encodeURI(urlPath);
},
genQrcode: function (name, title) {
var urlPath = this.genInstallURL(name, true);
$("#qrcode-title").html(title || name || location.pathname);
$("#qrcode-link").attr("href", urlPath);
$('#qrcodeCanvas').empty().qrcode({
text: encodeURI(urlPath),
});
$("#qrcodeRight a").attr("href", urlPath);
$("#qrcode-modal").modal("show");
},
genDownloadURL: function (f) {
var search = location.search;
var sep = search == "" ? "?" : "&"
return location.origin + this.getEncodePath(f) + location.search + sep + "download=true";
},
shouldHaveQrcode: function (name) {
return ['apk', 'ipa'].indexOf(getExtention(name)) !== -1;
},
genFileClass: function (f) {
if (f.type == "dir") {
if (f.name == '.git') {
return 'fa-git-square';
}
return "fa-folder-open";
}
var ext = getExtention(f.name);
switch (ext) {
case "go":
case "py":
case "js":
case "java":
case "c":
case "cpp":
case "h":
return "fa-file-code-o";
case "pdf":
return "fa-file-pdf-o";
case "zip":
return "fa-file-zip-o";
case "mp3":
case "wav":
return "fa-file-audio-o";
case "jpg":
case "png":
case "gif":
case "jpeg":
case "tiff":
return "fa-file-picture-o";
case "ipa":
case "dmg":
return "fa-apple";
case "apk":
return "fa-android";
case "exe":
return "fa-windows";
}
return "fa-file-text-o"
},
clickFileOrDir: function (f, e) {
var reqPath = pathJoin([location.pathname, encodeURIComponent(f.name)]);
// TODO: fix here tomorrow
if (f.type == "file") {
window.location.href = reqPath;
return;
}
loadFileOrDir(reqPath);
e.preventDefault()
},
changePath: function (reqPath, e) {
loadFileOrDir(reqPath);
e.preventDefault()
},
showInfo: function (f) {
console.log(f);
$.ajax({
url: pathJoin(["/", location.pathname, encodeURIComponent(f.name)]),
data: {
op: "info",
},
method: "GET",
success: function (res) {
$("#file-info-title").text(f.name);
$("#file-info-content").text(JSON.stringify(res, null, 4));
$("#file-info-modal").modal("show");
// console.log(JSON.stringify(res, null, 4));
},
error: function (jqXHR, textStatus, errorThrown) {
showErrorMessage(jqXHR)
}
})
},
makeDirectory: function () {
var name = window.prompt("current path: " + location.pathname + "\nplease enter the new directory name", "")
console.log(name)
if (!name) {
return
}
if(!checkPathNameLegal(name)) {
alert("Name should not contains any of \\/:*<>|")
return
}
$.ajax({
url: pathJoin(["/", location.pathname, "/", encodeURIComponent(name)]),
method: "POST",
success: function (res) {
console.log(res)
loadFileList()
},
error: function (jqXHR, textStatus, errorThrown) {
showErrorMessage(jqXHR)
}
})
},
deletePathConfirm: function (f, e) {
e.preventDefault();
if (!e.altKey) { // skip confirm when alt pressed
if (!window.confirm("Delete " + location.pathname + "/" + f.name + " ?")) {
return;
}
}
$.ajax({
url: pathJoin([location.pathname, encodeURIComponent(f.name)]),
method: 'DELETE',
success: function (res) {
loadFileList()
},
error: function (jqXHR, textStatus, errorThrown) {
showErrorMessage(jqXHR)
}
});
},
updateBreadcrumb: function (pathname) {
var pathname = decodeURI(pathname || location.pathname || "/");
pathname = pathname.split('?')[0]
var parts = pathname.split('/');
this.breadcrumb = [];
if (pathname == "/") {
return this.breadcrumb;
}
var i = 2;
for (; i <= parts.length; i += 1) {
var name = parts[i - 1];
if (!name) {
continue;
}
var path = parts.slice(0, i).join('/');
this.breadcrumb.push({
name: name + (i == parts.length ? ' /' : ''),
path: path
})
}
return this.breadcrumb;
},
loadPreviewFile: function (filepath, e) {
if (e) {
e.preventDefault() // may be need a switch
}
var that = this;
$.getJSON(pathJoin(['/-/info', location.pathname]))
.then(function (res) {
console.log(res);
that.preview.filename = res.name;
that.preview.filesize = res.size;
return $.ajax({
url: '/' + res.path,
dataType: 'text',
});
})
.then(function (res) {
console.log(res)
that.preview.contentHTML = '<pre>' + res + '</pre>';
console.log("Finally")
})
.done(function (res) {
console.log("done", res)
});
},
loadAll: function () {
// TODO: move loadFileList here
},
}
})
window.onpopstate = function (event) {
if (location.search.match(/\?search=/)) {
location.reload();
return;
}
loadFileList()
}
function loadFileOrDir(reqPath) {
let requestUri = reqPath + location.search
var retObj = loadFileList(requestUri)
if (retObj !== null) {
retObj.done(function () {
window.history.pushState({}, "", requestUri);
});
}
}
function loadFileList(pathname) {
var pathname = pathname || location.pathname + location.search;
var retObj = null
if (getQueryString("raw") !== "false") { // not a file preview
var sep = pathname.indexOf("?") === -1 ? "?" : "&"
retObj = $.ajax({
url: pathname + sep + "json=true",
dataType: "json",
cache: false,
success: function (res) {
res.files = _.sortBy(res.files, function (f) {
var weight = f.type == 'dir' ? 1000 : 1;
return -weight * f.mtime;
})
vm.files = res.files;
vm.auth = res.auth;
vm.updateBreadcrumb(pathname);
},
error: function (jqXHR, textStatus, errorThrown) {
showErrorMessage(jqXHR)
},
});
}
vm.previewMode = getQueryString("raw") == "false";
if (vm.previewMode) {
vm.loadPreviewFile();
}
return retObj
}
Vue.filter('fromNow', function (value) {
return moment(value).fromNow();
})
Vue.filter('formatBytes', function (value) {
var bytes = parseFloat(value);
if (bytes < 0) return "-";
else if (bytes < 1024) return bytes + " B";
else if (bytes < 1048576) return (bytes / 1024).toFixed(0) + " KB";
else if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + " MB";
else return (bytes / 1073741824).toFixed(1) + " GB";
})
$(function () {
$.scrollUp({
scrollText: '', // text are defined in css
});
// For page first loading
loadFileList(location.pathname + location.search)
// update version
$.getJSON("/-/sysinfo", function (res) {
vm.version = res.version;
})
var clipboard = new Clipboard('.btn');
clipboard.on('success', function (e) {
console.info('Action:', e.action);
console.info('Text:', e.text);
console.info('Trigger:', e.trigger);
$(e.trigger)
.tooltip('show')
.mouseleave(function () {
$(this).tooltip('hide');
})
e.clearSelection();
});
});

7
assets/js/jquery.scrollUp.min.js vendored Normal file
View file

@ -0,0 +1,7 @@
/*!
* scrollup v2.4.1
* Url: http://markgoodyear.com/labs/scrollup/
* Copyright (c) Mark Goodyear @markgdyr http://markgoodyear.com
* License: MIT
*/
!function(l,o,e){"use strict";l.fn.scrollUp=function(o){l.data(e.body,"scrollUp")||(l.data(e.body,"scrollUp",!0),l.fn.scrollUp.init(o))},l.fn.scrollUp.init=function(r){var s,t,c,i,n,a,d,p=l.fn.scrollUp.settings=l.extend({},l.fn.scrollUp.defaults,r),f=!1;switch(d=p.scrollTrigger?l(p.scrollTrigger):l("<a/>",{id:p.scrollName,href:"#top"}),p.scrollTitle&&d.attr("title",p.scrollTitle),d.appendTo("body"),p.scrollImg||p.scrollTrigger||d.html(p.scrollText),d.css({display:"none",position:"fixed",zIndex:p.zIndex}),p.activeOverlay&&l("<div/>",{id:p.scrollName+"-active"}).css({position:"absolute",top:p.scrollDistance+"px",width:"100%",borderTop:"1px dotted"+p.activeOverlay,zIndex:p.zIndex}).appendTo("body"),p.animation){case"fade":s="fadeIn",t="fadeOut",c=p.animationSpeed;break;case"slide":s="slideDown",t="slideUp",c=p.animationSpeed;break;default:s="show",t="hide",c=0}i="top"===p.scrollFrom?p.scrollDistance:l(e).height()-l(o).height()-p.scrollDistance,n=l(o).scroll(function(){l(o).scrollTop()>i?f||(d[s](c),f=!0):f&&(d[t](c),f=!1)}),p.scrollTarget?"number"==typeof p.scrollTarget?a=p.scrollTarget:"string"==typeof p.scrollTarget&&(a=Math.floor(l(p.scrollTarget).offset().top)):a=0,d.click(function(o){o.preventDefault(),l("html, body").animate({scrollTop:a},p.scrollSpeed,p.easingType)})},l.fn.scrollUp.defaults={scrollName:"scrollUp",scrollDistance:300,scrollFrom:"top",scrollSpeed:300,easingType:"linear",animation:"fade",animationSpeed:200,scrollTrigger:!1,scrollTarget:!1,scrollText:"Scroll to top",scrollTitle:!1,scrollImg:!1,activeOverlay:!1,zIndex:2147483647},l.fn.scrollUp.destroy=function(r){l.removeData(e.body,"scrollUp"),l("#"+l.fn.scrollUp.settings.scrollName).remove(),l("#"+l.fn.scrollUp.settings.scrollName+"-active").remove(),l.fn.jquery.split(".")[1]>=7?l(o).off("scroll",r):l(o).unbind("scroll",r)},l.scrollUp=l.fn.scrollUp}(jQuery,window,document);

7
assets/js/moment.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

5
assets/js/showdown-1.6.4.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

6
assets/js/underscore-min.js vendored Normal file

File diff suppressed because one or more lines are too long

23
assets/themes/black.css Normal file
View file

@ -0,0 +1,23 @@
body {}
td>a {
color: black;
}
.navbar {
border-radius: 0px;
background-color: #101010;
border-color: #101010;
}
.navbar .navbar-brand {
color: #9d9d9d;
}
.navbar .navbar-brand:hover {
color: #fff;
}
.navbar-default ul.navbar-nav>li>a:hover {
color: #fff;
}

31
assets/themes/cyan.css Normal file
View 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;
}

View file

@ -13,15 +13,19 @@ a:hover {
font-weight: bold;
}
.navbar-inverse {
.navbar {
background-color: #1b926c;
border-color: #1fa67a;
}
.navbar-inverse .navbar-brand {
.navbar .navbar-brand {
color: white;
}
.navbar {
border-radius: 0px;
}
.navbar-default ul.navbar-nav>li>a {
color: white;
}

9
assets_dev.go Normal file
View file

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

23
assets_generate.go Normal file
View file

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

26
build.sh Normal file → Executable file
View file

@ -2,13 +2,35 @@
set -eu
VERSION=$(git describe --abbrev=0 --tags)
REVCNT=$(git rev-list --count HEAD)
DEVCNT=$(git rev-list --count $VERSION)
if test $REVCNT != $DEVCNT
then
VERSION="$VERSION.dev$(expr $REVCNT - $DEVCNT)"
fi
echo "VER: $VERSION"
GITCOMMIT=$(git rev-parse HEAD)
BUILDTIME=$(date -u +%Y/%m/%d-%H:%M:%S)
LDFLAGS="-X main.VERSION=$VERSION -X main.BUILDTIME=$BUILDTIME -X main.GITCOMMIT=$GITCOMMIT"
if [[ -n "${EX_LDFLAGS:-""}" ]]
then
LDFLAGS="$LDFLAGS $EX_LDFLAGS"
fi
build() {
echo "$1 $2 ..."
GOOS=$1 GOARCH=$2 go build -tags bindata -o dist/gohttpserver-${3:-""}
GOOS=$1 GOARCH=$2 go build \
-tags vfs \
-ldflags "$LDFLAGS" \
-o dist/gohttpserver-${3:-""}
}
go-bindata-assetfs -tags bindata res/...
go generate .
build linux arm linux-arm
build darwin amd64 mac-amd64
build linux amd64 linux-amd64
build linux 386 linux-386

14
docker/Dockerfile Normal file
View file

@ -0,0 +1,14 @@
FROM docker.io/golang:1.16 AS build
WORKDIR /app/gohttpserver
ADD . /app/gohttpserver
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags '-X main.VERSION=docker' -o gohttpserver
FROM docker.io/alpine:latest
RUN mkdir -p /app/public
VOLUME /app/public
WORKDIR /app
ADD assets ./assets
COPY --from=build /app/gohttpserver/gohttpserver .
EXPOSE 8000
ENTRYPOINT [ "/app/gohttpserver", "--root=/app/public" ]
CMD []

15
docker/Dockerfile.armhf Normal file
View 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
View 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
View file

@ -0,0 +1,28 @@
#!/bin/bash
#
# push manifest
if [[ ! -d $HOME/.docker ]]
then
mkdir $HOME/.docker
fi
set -ex
if test $(uname) = "Linux"
then
sed -i '/experimental/d' $HOME/.docker/config.json
sed -i '1a"experimental": "enabled",' $HOME/.docker/config.json
fi
docker manifest create codeskyblue/gohttpserver \
codeskyblue/gohttpserver:latest \
codeskyblue/gohttpserver:armhf
docker manifest annotate codeskyblue/gohttpserver \
codeskyblue/gohttpserver:latest --os linux --arch amd64
docker manifest annotate codeskyblue/gohttpserver \
codeskyblue/gohttpserver:armhf --os linux --arch arm --variant v7
docker manifest push codeskyblue/gohttpserver
# check again
docker run mplatform/mquery codeskyblue/gohttpserver

27
go.mod Normal file
View file

@ -0,0 +1,27 @@
module github.com/hamburghammer/gohttpserver
go 1.16
require (
github.com/alecthomas/kingpin v2.2.6+incompatible
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect
github.com/codeskyblue/dockerignore v0.0.0-20151214070507-de82dee623d9
github.com/codeskyblue/go-accesslog v0.0.0-20171215023101-6188d3bd9371
github.com/codeskyblue/openid-go v0.0.0-20160923065855-0d30842b2fb4
github.com/fork2fix/go-plist v0.0.0-20181126021357-36960be5e636
github.com/go-yaml/yaml v2.1.0+incompatible
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d
github.com/gorilla/handlers v1.4.0
github.com/gorilla/mux v1.6.2
github.com/gorilla/sessions v1.1.3
github.com/pkg/errors v0.8.0 // indirect
github.com/shogo82148/androidbinary v0.0.0-20180627093851-01c4bfa8b3b5
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371 // indirect
github.com/shurcooL/vfsgen v0.0.0-20181020040650-a97a25d856ca
github.com/smartystreets/goconvey v1.6.4 // indirect
github.com/stretchr/testify v1.3.0
golang.org/x/text v0.3.3
golang.org/x/tools v0.1.0 // indirect
howett.net/plist v0.0.0-20201203080718-1454fab16a06 // indirect
)

91
go.sum Normal file
View file

@ -0,0 +1,91 @@
github.com/alecthomas/kingpin v2.2.6+incompatible h1:5svnBTFgJjZvGKyYBtMB0+m5wvrbUHiqye8wRJMlnYI=
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/codeskyblue/dockerignore v0.0.0-20151214070507-de82dee623d9 h1:c9axcChJwkLuSl9AvwTHi8jiBa6+VX4gGgERhABgv2E=
github.com/codeskyblue/dockerignore v0.0.0-20151214070507-de82dee623d9/go.mod h1:XNZkUhPf+qgRnhY/ecS3B73ODJ2NXCzDMJHXM069IMg=
github.com/codeskyblue/go-accesslog v0.0.0-20171215023101-6188d3bd9371 h1:dEBIvaVFaP2Uc9QA6J41qWxE5NfEnDWEBk+kWv5nK5k=
github.com/codeskyblue/go-accesslog v0.0.0-20171215023101-6188d3bd9371/go.mod h1:sgXnVxxZ1u72GAzc9s1SzpuPMxBDKfTg6F2PvDrPSJU=
github.com/codeskyblue/openid-go v0.0.0-20160923065855-0d30842b2fb4 h1:66lzN78lwccK+BPztRgBiWCYzhlerQEVOh2oeBksu5I=
github.com/codeskyblue/openid-go v0.0.0-20160923065855-0d30842b2fb4/go.mod h1:K/hSCtAHvnE9aM+LsYgVmgzPNFuWFdx6i9t6/3jNrZQ=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fork2fix/go-plist v0.0.0-20181126021357-36960be5e636 h1:ESUdS2eb8LyDQfboYyFBwAL+rqYhnTZ15ntw8BLsd9g=
github.com/fork2fix/go-plist v0.0.0-20181126021357-36960be5e636/go.mod h1:v6KRhgoO1QKamoeuZ7yHqZIP8p6j9k41Tb0jCyOEmr4=
github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d h1:lBXNCxVENCipq4D1Is42JVOP4eQjlB8TQ6H69Yx5J9Q=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/handlers v1.4.0 h1:XulKRWSQK5uChr4pEgSE4Tc/OcmnU9GJuSwdog/tZsA=
github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/shogo82148/androidbinary v0.0.0-20180627093851-01c4bfa8b3b5 h1:bXRaUWl3Afe3F9YR5NU1U3UB5zjCHlu4im5p3J/LUYk=
github.com/shogo82148/androidbinary v0.0.0-20180627093851-01c4bfa8b3b5/go.mod h1:05AjXWPWLdTIl9+REKhSmTeoJ6Wz5e9ir0Q0NRxCIKo=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371 h1:SWV2fHctRpRrp49VXJ6UZja7gU9QLHwRpIPBN89SKEo=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/vfsgen v0.0.0-20181020040650-a97a25d856ca h1:3fECS8atRjByijiI8yYiuwLwQ2ZxXobW7ua/8GRB3pI=
github.com/shurcooL/vfsgen v0.0.0-20181020040650-a97a25d856ca/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
howett.net/plist v0.0.0-20201203080718-1454fab16a06 h1:QDxUo/w2COstK1wIBYpzQlHX/NqaQTcf9jyz347nI58=
howett.net/plist v0.0.0-20201203080718-1454fab16a06/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=

View file

@ -1,7 +1,11 @@
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"html/template"
"io"
"io/ioutil"
"log"
@ -10,107 +14,340 @@ import (
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"regexp"
"github.com/go-yaml/yaml"
"github.com/gorilla/mux"
"github.com/shogo82148/androidbinary/apk"
)
const YAMLCONF = ".ghs.yml"
type ApkInfo struct {
PackageName string `json:"packageName"`
MainActivity string `json:"mainActivity"`
Version struct {
Code int `json:"code"`
Name string `json:"name"`
} `json:"version"`
}
type IndexFileItem struct {
Path string
Info os.FileInfo
}
type HTTPStaticServer struct {
Root string
Theme string
Upload bool
Delete bool
Title string
Theme string
PlistProxy string
AuthType string
m *mux.Router
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 = "./"
}
root = filepath.ToSlash(root)
if !strings.HasSuffix(root, "/") {
root = root + "/"
}
log.Printf("root path: %s\n", root)
m := mux.NewRouter()
s := &HTTPStaticServer{
Root: root,
Theme: "black",
m: m,
bufPool: sync.Pool{
New: func() interface{} { return make([]byte, 32*1024) },
},
}
m.HandleFunc("/-/status", s.hStatus)
m.HandleFunc("/-/raw/{path:.*}", s.hFileOrDirectory)
m.HandleFunc("/-/zip/{path:.*}", s.hZip)
m.HandleFunc("/-/unzip/{zip_path:.*}/-/{path:.*}", s.hUnzip)
m.HandleFunc("/-/json/{path:.*}", s.hJSONList)
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)
}
}()
// routers for Apple *.ipa
m.HandleFunc("/-/ipa/plist/{path:.*}", s.hPlist)
m.HandleFunc("/-/ipa/link/{path:.*}", s.hIpaLink)
// TODO: /ipa/info
m.HandleFunc("/{path:.*}", s.hIndex).Methods("GET")
m.HandleFunc("/{path:.*}", s.hIndex).Methods("GET", "HEAD")
m.HandleFunc("/{path:.*}", s.hUploadOrMkdir).Methods("POST")
m.HandleFunc("/{path:.*}", s.hDelete).Methods("DELETE")
return s
}
func (s *HTTPStaticServer) EnableUpload() {
s.Upload = true
s.m.HandleFunc("/{path:.*}", s.hUpload).Methods("POST")
}
func (s *HTTPStaticServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.m.ServeHTTP(w, r)
}
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) hUpload(w http.ResponseWriter, req *http.Request) {
err := req.ParseMultipartForm(1 << 30) // max memory 1G
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if len(req.MultipartForm.File["file"]) == 0 {
http.Error(w, "Need multipart file", http.StatusInternalServerError)
return
}
path := mux.Vars(req)["path"]
dirpath := filepath.Join(s.Root, path)
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
}
}
w.Write([]byte("Upload success"))
}
func (s *HTTPStaticServer) hIndex(w http.ResponseWriter, r *http.Request) {
path := mux.Vars(r)["path"]
relPath := filepath.Join(s.Root, path)
finfo, err := os.Stat(relPath)
if err == nil && finfo.IsDir() {
tmpl.ExecuteTemplate(w, "index", s)
// tmpl.Execute(w, s)
if r.FormValue("json") == "true" {
s.hJSONList(w, r)
return
}
if r.FormValue("op") == "info" {
s.hInfo(w, r)
return
}
if r.FormValue("op") == "archive" {
s.hZip(w, r)
return
}
log.Println("GET", path, relPath)
if r.FormValue("raw") == "false" || isDir(relPath) {
if r.Method == "HEAD" {
return
}
renderHTML(w, "index.html", s)
} else {
if filepath.Base(path) == YAMLCONF {
auth := s.readAccessConf(path)
if !auth.Delete {
http.Error(w, "Security warning, not allowed to read", http.StatusForbidden)
return
}
}
if r.FormValue("download") == "true" {
w.Header().Set("Content-Disposition", "attachment; filename="+strconv.Quote(filepath.Base(path)))
}
http.ServeFile(w, r, relPath)
}
}
func (s *HTTPStaticServer) hMkdir(w http.ResponseWriter, req *http.Request) {
path := filepath.Dir(mux.Vars(req)["path"])
auth := s.readAccessConf(path)
if !auth.canDelete(req) {
http.Error(w, "Mkdir forbidden", http.StatusForbidden)
return
}
name := filepath.Base(mux.Vars(req)["path"])
if err := checkFilename(name); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
err := os.Mkdir(filepath.Join(s.Root, path, name), 0755)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
w.Write([]byte("Success"))
}
func (s *HTTPStaticServer) hDelete(w http.ResponseWriter, req *http.Request) {
path := mux.Vars(req)["path"]
path = filepath.Clean(path) // for safe reason, prevent path contain ..
auth := s.readAccessConf(path)
if !auth.canDelete(req) {
http.Error(w, "Delete forbidden", http.StatusForbidden)
return
}
// TODO: path safe check
err := os.RemoveAll(filepath.Join(s.Root, path))
if err != nil {
pathErr, ok := err.(*os.PathError)
if ok {
http.Error(w, pathErr.Op+" "+path+": "+pathErr.Err.Error(), 500)
} else {
http.Error(w, err.Error(), 500)
}
return
}
w.Write([]byte("Success"))
}
func (s *HTTPStaticServer) hUploadOrMkdir(w http.ResponseWriter, req *http.Request) {
path := mux.Vars(req)["path"]
dirpath := filepath.Join(s.Root, path)
// check auth
auth := s.readAccessConf(path)
if !auth.canUpload(req) {
http.Error(w, "Upload forbidden", http.StatusForbidden)
return
}
file, header, err := req.FormFile("file")
if _, err := os.Stat(dirpath); os.IsNotExist(err) {
if err := os.MkdirAll(dirpath, os.ModePerm); err != nil {
log.Println("Create directory:", err)
http.Error(w, "Directory create "+err.Error(), http.StatusInternalServerError)
return
}
}
if file == nil { // only mkdir
w.Header().Set("Content-Type", "application/json;charset=utf-8")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"destination": dirpath,
})
return
}
if err != nil {
log.Println("Parse form file:", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
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
}
dstPath := filepath.Join(dirpath, filename)
// Large file (>32MB) will store in tmp directory
// The quickest operation is call os.Move instead of os.Copy
// Note: it seems not working well, os.Rename might be failed
var copyErr error
// if osFile, ok := file.(*os.File); ok && fileExists(osFile.Name()) {
// tmpUploadPath := osFile.Name()
// osFile.Close() // Windows can not rename opened file
// log.Printf("Move %s -> %s", tmpUploadPath, dstPath)
// copyErr = os.Rename(tmpUploadPath, dstPath)
// } else {
dst, err := os.Create(dstPath)
if err != nil {
log.Println("Create file:", err)
http.Error(w, "File create "+err.Error(), http.StatusInternalServerError)
return
}
// Note: very large size file might cause poor performance
// _, copyErr = io.Copy(dst, file)
buf := s.bufPool.Get().([]byte)
defer s.bufPool.Put(buf)
_, copyErr = io.CopyBuffer(dst, file, buf)
dst.Close()
// }
if copyErr != nil {
log.Println("Handle upload file:", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json;charset=utf-8")
if req.FormValue("unzip") == "true" {
err = unzipFile(dstPath, dirpath)
os.Remove(dstPath)
message := "success"
if err != nil {
message = err.Error()
}
json.NewEncoder(w).Encode(map[string]interface{}{
"success": err == nil,
"description": message,
})
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"destination": dstPath,
})
}
type FileJSONInfo struct {
Name string `json:"name"`
Type string `json:"type"`
Size int64 `json:"size"`
Path string `json:"path"`
ModTime int64 `json:"mtime"`
Extra interface{} `json:"extra,omitempty"`
}
// path should be absolute
func parseApkInfo(path string) (ai *ApkInfo) {
defer func() {
if err := recover(); err != nil {
log.Println("parse-apk-info panic:", err)
}
}()
apkf, err := apk.OpenFile(path)
if err != nil {
return
}
ai = &ApkInfo{}
ai.MainActivity, _ = apkf.MainActivity()
ai.PackageName = apkf.PackageName()
ai.Version.Code = apkf.Manifest().VersionCode
ai.Version.Name = apkf.Manifest().VersionName
return
}
func (s *HTTPStaticServer) hInfo(w http.ResponseWriter, r *http.Request) {
path := mux.Vars(r)["path"]
relPath := filepath.Join(s.Root, path)
fi, err := os.Stat(relPath)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
fji := &FileJSONInfo{
Name: fi.Name(),
Size: fi.Size(),
Path: path,
ModTime: fi.ModTime().UnixNano() / 1e6,
}
ext := filepath.Ext(path)
switch ext {
case ".md":
fji.Type = "markdown"
case ".apk":
fji.Type = "apk"
fji.Extra = parseApkInfo(relPath)
case "":
fji.Type = "dir"
default:
fji.Type = "text"
}
data, _ := json.Marshal(fji)
w.Header().Set("Content-Type", "application/json")
w.Write(data)
}
func (s *HTTPStaticServer) hZip(w http.ResponseWriter, r *http.Request) {
path := mux.Vars(r)["path"]
CompressToZip(w, filepath.Join(s.Root, path))
@ -130,13 +367,9 @@ func (s *HTTPStaticServer) hUnzip(w http.ResponseWriter, r *http.Request) {
}
}
func genURLStr(r *http.Request, path string) *url.URL {
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
func combineURL(r *http.Request, path string) *url.URL {
return &url.URL{
Scheme: scheme,
Scheme: r.URL.Scheme,
Host: r.Host,
Path: path,
}
@ -175,72 +408,331 @@ 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 s.PlistProxy != "" {
plistUrl = strings.TrimSuffix(s.PlistProxy, "/") + "/" + r.Host + "/-/ipa/plist/" + path
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 {
http.Error(w, err.Error(), 500)
return
}
plistUrl = url
} else {
http.Error(w, "500: Server should be https:// or provide valid plistproxy", 500)
return
}
w.Header().Set("Content-Type", "text/html")
tmpl.ExecuteTemplate(w, "ipa-install", map[string]string{
log.Println("PlistURL:", plistUrl)
renderHTML(w, "ipa-install.html", map[string]string{
"Name": filepath.Base(path),
"PlistLink": plistUrl,
})
// w.Write([]byte(fmt.Sprintf(
// `<a href='itms-services://?action=download-manifest&url=%s'>Click this link to install</a>`,
// plistUrl)))
}
func (s *HTTPStaticServer) genPlistLink(httpPlistLink string) (plistUrl string, err error) {
// Maybe need a proxy, a little slowly now.
pp := s.PlistProxy
if pp == "" {
pp = defaultPlistProxy
}
resp, err := http.Get(httpPlistLink)
if err != nil {
return
}
defer resp.Body.Close()
data, _ := ioutil.ReadAll(resp.Body)
retData, err := http.Post(pp, "text/xml", bytes.NewBuffer(data))
if err != nil {
return
}
defer retData.Body.Close()
jsonData, _ := ioutil.ReadAll(retData.Body)
var ret map[string]string
if err = json.Unmarshal(jsonData, &ret); err != nil {
return
}
plistUrl = pp + "/" + ret["key"]
return
}
func (s *HTTPStaticServer) hFileOrDirectory(w http.ResponseWriter, r *http.Request) {
path := mux.Vars(r)["path"]
log.Println("Path:", s.Root, path)
http.ServeFile(w, r, filepath.Join(s.Root, path))
}
type ListResponse struct {
Name string `json:"name"`
Path string `json:"path"`
Type string `json:"type"`
Size string `json:"size"`
type HTTPFileInfo struct {
Name string `json:"name"`
Path string `json:"path"`
Type string `json:"type"`
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"`
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) {
path := mux.Vars(r)["path"]
lrs := make([]ListResponse, 0)
fd, err := os.Open(filepath.Join(s.Root, path))
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer fd.Close()
requestPath := mux.Vars(r)["path"]
localPath := filepath.Join(s.Root, requestPath)
search := r.FormValue("search")
auth := s.readAccessConf(requestPath)
auth.Upload = auth.canUpload(r)
auth.Delete = auth.canDelete(r)
files, err := fd.Readdir(-1)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
for _, file := range files {
lr := ListResponse{
Name: file.Name(),
Path: filepath.Join(path, file.Name()), // lstrip "/"
// path string -> info os.FileInfo
fileInfoMap := make(map[string]os.FileInfo, 0)
if search != "" {
results := s.findIndex(search)
if len(results) > 50 { // max 50
results = results[:50]
}
if file.IsDir() {
fileName := deepPath(filepath.Join(s.Root, path), file.Name())
lr.Name = fileName
lr.Path = filepath.Join(path, fileName)
for _, item := range results {
if filepath.HasPrefix(item.Path, requestPath) {
fileInfoMap[item.Path] = item.Info
}
}
} else {
infos, err := ioutil.ReadDir(localPath)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
for _, info := range infos {
fileInfoMap[filepath.Join(requestPath, info.Name())] = info
}
}
// turn file list -> json
lrs := make([]HTTPFileInfo, 0)
for path, info := range fileInfoMap {
if !auth.canAccess(info.Name()) {
continue
}
lr := HTTPFileInfo{
Name: info.Name(),
Path: path,
ModTime: info.ModTime().UnixNano() / 1e6,
}
if search != "" {
name, err := filepath.Rel(requestPath, path)
if err != nil {
log.Println(requestPath, path, err)
}
lr.Name = filepath.ToSlash(name) // fix for windows
}
if info.IsDir() {
name := deepPath(localPath, info.Name())
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(file)
lr.Size = info.Size() // formatSize(info)
}
lrs = append(lrs, lr)
}
data, _ := json.Marshal(lrs)
data, _ := json.Marshal(map[string]interface{}{
"files": lrs,
"auth": auth,
})
w.Header().Set("Content-Type", "application/json")
w.Write(data)
}
var dirSizeMap = make(map[string]int64)
func (s *HTTPStaticServer) makeIndex() error {
var indexes = make([]IndexFileItem, 0)
var err = filepath.Walk(s.Root, func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Printf("WARN: Visit path: %s error: %v", strconv.Quote(path), err)
return filepath.SkipDir
// return err
}
if info.IsDir() {
return nil
}
path, _ = filepath.Rel(s.Root, path)
path = filepath.ToSlash(path)
indexes = append(indexes, IndexFileItem{path, info})
return nil
})
s.indexes = indexes
dirSizeMap = make(map[string]int64)
return err
}
func (s *HTTPStaticServer) historyDirSize(dir string) int64 {
var size int64
if size, ok := dirSizeMap[dir]; ok {
return size
}
for _, fitem := range s.indexes {
if filepath.HasPrefix(fitem.Path, dir) {
size += fitem.Info.Size()
}
}
dirSizeMap[dir] = size
return size
}
func (s *HTTPStaticServer) findIndex(text string) []IndexFileItem {
ret := make([]IndexFileItem, 0)
for _, item := range s.indexes {
ok := true
// search algorithm, space for AND
for _, keyword := range strings.Fields(text) {
needContains := true
if strings.HasPrefix(keyword, "-") {
needContains = false
keyword = keyword[1:]
}
if keyword == "" {
continue
}
ok = (needContains == strings.Contains(strings.ToLower(item.Path), strings.ToLower(keyword)))
if !ok {
break
}
}
if ok {
ret = append(ret, item)
}
}
return ret
}
func (s *HTTPStaticServer) defaultAccessConf() AccessConf {
return AccessConf{
Upload: s.Upload,
Delete: s.Delete,
}
}
func (s *HTTPStaticServer) readAccessConf(requestPath string) (ac AccessConf) {
requestPath = filepath.Clean(requestPath)
if requestPath == "/" || requestPath == "" || requestPath == "." {
ac = s.defaultAccessConf()
} else {
parentPath := filepath.Dir(requestPath)
ac = s.readAccessConf(parentPath)
}
relPath := filepath.Join(s.Root, requestPath)
if isFile(relPath) {
relPath = filepath.Dir(relPath)
}
cfgFile := filepath.Join(relPath, YAMLCONF)
data, err := ioutil.ReadFile(cfgFile)
if err != nil {
if os.IsNotExist(err) {
return
}
log.Printf("Err read .ghs.yml: %v", err)
}
err = yaml.Unmarshal(data, &ac)
if err != nil {
log.Printf("Err format .ghs.yml: %v", err)
}
return
}
func deepPath(basedir, name string) string {
isDir := true
// loop max 5, incase of for loop not finished
@ -251,10 +743,85 @@ func deepPath(basedir, name string) string {
break
}
if finfos[0].IsDir() {
name = filepath.Join(name, finfos[0].Name())
name = filepath.ToSlash(filepath.Join(name, finfos[0].Name()))
} else {
break
}
}
return name
}
func isFile(path string) bool {
info, err := os.Stat(path)
return err == nil && info.Mode().IsRegular()
}
func isDir(path string) bool {
info, err := os.Stat(path)
return err == nil && info.Mode().IsDir()
}
func assetsContent(name string) string {
fd, err := Assets.Open(name)
if err != nil {
panic(err)
}
data, err := ioutil.ReadAll(fd)
if err != nil {
panic(err)
}
return string(data)
}
// TODO: I need to read more abouthtml/template
var (
funcMap template.FuncMap
)
func init() {
funcMap = template.FuncMap{
"title": strings.Title,
"urlhash": func(path string) string {
httpFile, err := Assets.Open(path)
if err != nil {
return path + "#no-such-file"
}
info, err := httpFile.Stat()
if err != nil {
return path + "#stat-error"
}
return fmt.Sprintf("%s?t=%d", path, info.ModTime().Unix())
},
}
}
var (
_tmpls = make(map[string]*template.Template)
)
func executeTemplate(w http.ResponseWriter, name string, v interface{}) {
if t, ok := _tmpls[name]; ok {
t.Execute(w, v)
return
}
t := template.Must(template.New(name).Funcs(funcMap).Delims("[[", "]]").Parse(assetsContent(name)))
_tmpls[name] = t
t.Execute(w, v)
}
func renderHTML(w http.ResponseWriter, name string, v interface{}) {
if _, ok := Assets.(http.Dir); ok {
log.Println("Hot load", name)
t := template.Must(template.New(name).Funcs(funcMap).Delims("[[", "]]").Parse(assetsContent(name)))
t.Execute(w, v)
} else {
executeTemplate(w, name, v)
}
}
func checkFilename(name string) error {
if strings.ContainsAny(name, "\\/:*<>|") {
return errors.New("Name should not contains \\/:*<>|")
}
return nil
}

3
ipa.go
View file

@ -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) {

196
main.go
View file

@ -1,95 +1,211 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
"os"
"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/shurcooL/vfsgen"
)
type Configure struct {
Addr string
Root string
HttpAuth string
Cert string
Key string
Cors bool
Theme string
XProxy bool
Upload bool
PlistProxy *url.URL
Conf *os.File `yaml:"-"`
Addr string `yaml:"addr"`
Port int `yaml:"port"`
Root string `yaml:"root"`
HTTPAuth string `yaml:"httpauth"`
Cert string `yaml:"cert"`
Key string `yaml:"key"`
Cors bool `yaml:"cors"`
Theme string `yaml:"theme"`
XHeaders bool `yaml:"xheaders"`
Upload bool `yaml:"upload"`
Delete bool `yaml:"delete"`
PlistProxy string `yaml:"plistproxy"`
Title string `yaml:"title"`
Debug bool `yaml:"debug"`
Auth struct {
Type string `yaml:"type"` // openid|http|github
OpenID string `yaml:"openid"`
HTTP string `yaml:"http"`
ID string `yaml:"id"` // for oauth2
Secret string `yaml:"secret"` // for oauth2
} `yaml:"auth"`
}
type logger struct {
}
type httpLogger struct{}
func (l logger) Log(record accesslog.LogRecord) {
log.Printf("%s [code %d] %s", record.Method, record.Status, record.Uri)
func (l httpLogger) Log(record accesslog.LogRecord) {
log.Printf("%s - %s %d %s", record.Ip, record.Method, record.Status, record.Uri)
}
var (
gcfg = Configure{}
l = logger{}
defaultPlistProxy = "https://plistproxy.herokuapp.com/plist"
defaultOpenID = "https://login.netease.com/openid"
gcfg = Configure{}
logger = httpLogger{}
VERSION = "unknown"
BUILDTIME = "unknown time"
GITCOMMIT = "unknown git commit"
SITE = "https://github.com/codeskyblue/gohttpserver"
)
func parseFlags() {
func versionMessage() string {
t := template.Must(template.New("version").Parse(`GoHTTPServer
Version: {{.Version}}
Go version: {{.GoVersion}}
OS/Arch: {{.OSArch}}
Git commit: {{.GitCommit}}
Built: {{.Built}}
Site: {{.Site}}`))
buf := bytes.NewBuffer(nil)
t.Execute(buf, map[string]interface{}{
"Version": VERSION,
"GoVersion": runtime.Version(),
"OSArch": runtime.GOOS + "/" + runtime.GOARCH,
"GitCommit": GITCOMMIT,
"Built": BUILDTIME,
"Site": SITE,
})
return buf.String()
}
func parseFlags() error {
// initial default conf
gcfg.Root = "./"
gcfg.Port = 8000
gcfg.Addr = ""
gcfg.Theme = "black"
gcfg.PlistProxy = defaultPlistProxy
gcfg.Auth.OpenID = defaultOpenID
gcfg.Title = "Go HTTP File Server"
kingpin.HelpFlag.Short('h')
kingpin.Flag("root", "root directory").Short('r').Default("./").StringVar(&gcfg.Root)
kingpin.Flag("addr", "listen address").Short('a').Default(":8000").StringVar(&gcfg.Addr)
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("port", "listen port, default 8000").IntVar(&gcfg.Port)
kingpin.Flag("addr", "listen address, eg 127.0.0.1:8000").Short('a').StringVar(&gcfg.Addr)
kingpin.Flag("cert", "tls cert.pem path").StringVar(&gcfg.Cert)
kingpin.Flag("key", "tls key.pem path").StringVar(&gcfg.Key)
kingpin.Flag("auth-type", "Auth type <http|openid>").StringVar(&gcfg.Auth.Type)
kingpin.Flag("auth-http", "HTTP basic auth (ex: user:pass)").StringVar(&gcfg.Auth.HTTP)
kingpin.Flag("auth-openid", "OpenID auth identity url").StringVar(&gcfg.Auth.OpenID)
kingpin.Flag("theme", "web theme, one of <black|green>").StringVar(&gcfg.Theme)
kingpin.Flag("upload", "enable upload support").BoolVar(&gcfg.Upload)
kingpin.Flag("delete", "enable delete support").BoolVar(&gcfg.Delete)
kingpin.Flag("xheaders", "used when behide nginx").BoolVar(&gcfg.XHeaders)
kingpin.Flag("cors", "enable cross-site HTTP request").BoolVar(&gcfg.Cors)
kingpin.Flag("httpauth", "HTTP basic auth (ex: user:pass)").Default("").StringVar(&gcfg.HttpAuth)
kingpin.Flag("theme", "web theme, one of <black|green>").Default("black").StringVar(&gcfg.Theme)
kingpin.Flag("xproxy", "Used when behide proxy like nginx").BoolVar(&gcfg.XProxy)
kingpin.Flag("upload", "Enable upload support").BoolVar(&gcfg.Upload)
kingpin.Flag("plistproxy", "IPA Plist file proxy, https needed").Short('p').URLVar(&gcfg.PlistProxy)
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.Parse()
kingpin.Parse() // first parse conf
if gcfg.Conf != nil {
defer func() {
kingpin.Parse() // command line priority high than conf
}()
ymlData, err := ioutil.ReadAll(gcfg.Conf)
if err != nil {
return err
}
return yaml.Unmarshal(ymlData, &gcfg)
}
return nil
}
func main() {
parseFlags()
if err := parseFlags(); err != nil {
log.Fatal(err)
}
if gcfg.Debug {
data, _ := yaml.Marshal(gcfg)
fmt.Printf("--- config ---\n%s\n", string(data))
}
log.SetFlags(log.Lshortfile | log.LstdFlags)
ss := NewHTTPStaticServer(gcfg.Root)
ss.Theme = gcfg.Theme
ss.Title = gcfg.Title
ss.Upload = gcfg.Upload
ss.Delete = gcfg.Delete
ss.AuthType = gcfg.Auth.Type
log.Println(gcfg.PlistProxy)
if gcfg.Upload {
ss.EnableUpload()
if gcfg.PlistProxy != "" {
u, err := url.Parse(gcfg.PlistProxy)
if err != nil {
log.Fatal(err)
}
u.Scheme = "https"
ss.PlistProxy = u.String()
}
if gcfg.PlistProxy != nil {
gcfg.PlistProxy.Scheme = "https"
ss.PlistProxy = gcfg.PlistProxy.String()
if ss.PlistProxy != "" {
log.Printf("plistproxy: %s", strconv.Quote(ss.PlistProxy))
}
var hdlr http.Handler = ss
hdlr = accesslog.NewLoggingHandler(hdlr, l)
hdlr = accesslog.NewLoggingHandler(hdlr, logger)
// HTTP Basic Authentication
userpass := strings.SplitN(gcfg.HttpAuth, ":", 2)
if len(userpass) == 2 {
user, pass := userpass[0], userpass[1]
hdlr = httpauth.SimpleBasicAuth(user, pass)(hdlr)
userpass := strings.SplitN(gcfg.Auth.HTTP, ":", 2)
switch gcfg.Auth.Type {
case "http":
if len(userpass) == 2 {
user, pass := userpass[0], userpass[1]
hdlr = httpauth.SimpleBasicAuth(user, pass)(hdlr)
}
case "openid":
handleOpenID(gcfg.Auth.OpenID, false) // FIXME(ssx): set secure default to false
// case "github":
// handleOAuth2ID(gcfg.Auth.Type, gcfg.Auth.ID, gcfg.Auth.Secret) // FIXME(ssx): set secure default to false
case "oauth2-proxy":
handleOauth2()
}
// CORS
if gcfg.Cors {
hdlr = handlers.CORS()(hdlr)
}
if gcfg.XProxy {
if gcfg.XHeaders {
hdlr = handlers.ProxyHeaders(hdlr)
}
http.Handle("/", hdlr)
http.Handle("/-/assets/", http.StripPrefix("/-/assets/", http.FileServer(Assets)))
http.HandleFunc("/-/sysinfo", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
data, _ := json.Marshal(map[string]interface{}{
"version": VERSION,
})
w.Write(data)
})
log.Printf("Listening on addr: %s\n", strconv.Quote(gcfg.Addr))
if gcfg.Addr == "" {
gcfg.Addr = fmt.Sprintf(":%d", gcfg.Port)
}
if !strings.Contains(gcfg.Addr, ":") {
gcfg.Addr = ":" + gcfg.Addr
}
_, port, _ := net.SplitHostPort(gcfg.Addr)
log.Printf("listening on %s, local address http://%s:%s\n", strconv.Quote(gcfg.Addr), getLocalIP(), port)
var err error
if gcfg.Key != "" && gcfg.Cert != "" {

27
oauth2-proxy.go Normal file
View 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
View 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)
})
}

View file

@ -1,182 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<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/style.css">
<link rel="stylesheet" type="text/css" href="/-/res/themes/[[.Theme]].css">
</head>
<body id="app">
<nav class="navbar navbar-inverse">
<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="/">Go HTTP File Server</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-2">
<ul class="nav navbar-nav">
<li><a href="{{.dir}}">{{.dir}}</a></li>
</ul>
<form class="navbar-form navbar-right">
<div class="input-group">
<input type="text" name="search" class="form-control" placeholder="Not finished.">
<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 colspan=3>
<td>
<button class="btn btn-xs btn-default" v-on:click='toggleHidden()'>
Show hidden <i class="fa" v-bind:class='showHidden ? "fa-eye" : "fa-eye-slash"'></i>
</button>
[[ if .Upload ]]
<button class="btn btn-xs btn-default" data-toggle="modal" data-target="#upload-modal">
Upload file <i class="fa fa-upload"></i>
</button>
[[ end ]]
</td>
</tr>
<tr>
<th>Name</th>
<th>Size</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 style="text-align: left">
<template v-if="f.type == 'dir'">
<a class="btn btn-default btn-xs" href="/-/zip/{{f.path}}">
Archieve Zip
<span class="glyphicon glyphicon-download-alt"></span>
</a>
</template>
<template v-if="f.type == 'file'">
<a class="btn btn-default btn-xs" href="/-/raw/{{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" v-if="shouldHaveQrcode(f.name)
" v-on:click="genQrcode(f.name)" href="#">
<span class="hidden-xs">QRCode</span>
<span class="glyphicon glyphicon-qrcode"></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">&times;</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">&times;</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</a>, written by <a href="https://github.com/codeskyblue">codeskyblue</a>. 2016. go 1.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/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/dropzone.js"></script>
<script src="/-/res/bootstrap-3.3.5/js/bootstrap.min.js"></script>
<script src="/-/res/js/index.js"></script>
</body>
</html>

View file

@ -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>

View file

@ -1,216 +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);
}
var vm = new Vue({
el: "#app",
data: {
message: "Hello vue.js",
breadcrumb: [],
showHidden: false,
previewFile: null,
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: {
toggleHidden: function() {
this.showHidden = !this.showHidden;
},
genQrcode: function(text) {
var urlPath = location.protocol + "//" + pathJoin([location.host, location.pathname, text]);
$("#qrcode-title").html(text);
$("#qrcode-link").attr("href", urlPath);
if (getExtention(text) == "ipa") {
urlPath = location.protocol + "//" + pathJoin([location.host, "/-/ipa/link", location.pathname, text]);
console.log(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":
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()
},
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()
}
function loadFileList() {
$.getJSON("/-/json" + location.pathname, function(res) {
// console.log(res)
res.sort(function(a, b) {
var obj2n = function(v) {
return v.type == "dir" ? 0 : 1;
}
return obj2n(a) - obj2n(b);
})
vm.files = res;
})
vm.updateBreadcrumb();
// if (Dropzone.options.myDropzone) {
// Dropzone.options.myDropzone.url = location.pathname;
// }
}
// For page first loading
loadFileList()
Dropzone.options.myDropzone = {
// url: location.pathname,
paramName: "file",
maxFilesize: 1024,
addRemoveLinks: true,
init: function() {
this.on("uploadprogress", function(file, progress) {
console.log("File progress", progress);
});
this.on("complete", function(file) {
loadFileList()
})
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,9 +0,0 @@
body {}
td>a {
color: black;
}
.navbar {
border-radius: 0px;
}

View file

@ -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))
}
}

View file

@ -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))
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

View file

@ -2,6 +2,7 @@
# coding: utf-8
import time
import hashlib
import tornado
import tornado.ioloop
@ -39,11 +40,34 @@ class ProxyHandler(tornado.web.RequestHandler):
self.write(response.body)
self.finish()
class PlistStoreHandler(tornado.web.RequestHandler):
db = {}
def post(self):
body = self.request.body
if len(body) > 5000:
self.set_status(500)
self.finish("request body too long")
m = hashlib.md5()
m.update(body)
key = m.hexdigest()[8:16]
self.db[key] = body
self.write({'key': key})
def get(self):
key = self.get_argument('key')
value = self.db.get(key)
if value is None:
raise tornado.web.HTTPError(404)
self.set_header('Content-Type', 'text/xml')
self.finish(value)
def make_app(debug=True):
return tornado.web.Application([
(r"/", MainHandler),
(r"/proxy/(.*)", ProxyHandler),
(r"/plist", PlistStoreHandler),
], debug=debug)
@ -51,4 +75,4 @@ if __name__ == "__main__":
app = make_app()
tornado.options.parse_command_line()
app.listen(options.port)
ioloop.IOLoop.current().start()
ioloop.IOLoop.current().start()

7
testdata/config.yml vendored Normal file
View file

@ -0,0 +1,7 @@
---
addr: ":4000"
title: "hello world"
theme: green
debug: true
xheaders: true
cors: true

7
testdata/deletable/.ghs.yml vendored Normal file
View file

@ -0,0 +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
View file

0
testdata/deletable/other.file vendored Normal file
View file

0
testdata/deletable/visual.file vendored Normal file
View file

BIN
testdata/filetypes/gohttpserver.gif vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 KiB

6
testdata/filetypes/page.html vendored Normal file
View file

@ -0,0 +1,6 @@
<html>
<title>Haha</title>
<body>
<h1>Hello world</h1>
</body>
</html>

4
testdata/filetypes/style.css vendored Normal file
View file

@ -0,0 +1,4 @@
body {
background-color: red;
}

7
testdata/uploadable/.ghs.yml vendored Normal file
View file

@ -0,0 +1,7 @@
---
upload: false
users:
- email: "user@example.com"
upload: true
delete: true
token: 123456

View file

View file

@ -1,28 +1,27 @@
package main
import (
"fmt"
"net"
"net/http"
"os"
"strconv"
"strings"
)
func formatSize(file os.FileInfo) string {
if file.IsDir() {
return "-"
}
size := file.Size()
switch {
case size > 1024*1024:
return fmt.Sprintf("%.1f MB", float64(size)/1024/1024)
case size > 1024:
return fmt.Sprintf("%.1f KB", float64(size)/1024)
default:
return strconv.Itoa(int(size)) + " B"
}
return ""
}
// func formatSize(file os.FileInfo) string {
// if file.IsDir() {
// return "-"
// }
// size := file.Size()
// switch {
// case size > 1024*1024:
// return fmt.Sprintf("%.1f MB", float64(size)/1024/1024)
// case size > 1024:
// return fmt.Sprintf("%.1f KB", float64(size)/1024)
// default:
// return strconv.Itoa(int(size)) + " B"
// }
// return ""
// }
func getRealIP(req *http.Request) string {
xip := req.Header.Get("X-Real-IP")
@ -37,8 +36,7 @@ func SublimeContains(s, substr string) bool {
if len(rsubstr) > len(rs) {
return false
}
// abcdefg
// df
var ok = true
var i, j = 0, 0
for ; i < len(rsubstr); i++ {
@ -57,3 +55,28 @@ func SublimeContains(s, substr string) bool {
}
return ok
}
// getLocalIP returns the non loopback local IP of the host
func getLocalIP() string {
addrs, err := net.InterfaceAddrs()
if err != nil {
return ""
}
for _, address := range addrs {
// check the address type and if it is not a loopback the display it
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
return ipnet.IP.String()
}
}
}
return ""
}
func fileExists(path string) bool {
info, err := os.Stat(path)
if err != nil {
return false
}
return !info.IsDir()
}

View file

@ -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.

View file

@ -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"})
}
```

View file

@ -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}
}

View file

@ -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
}

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

Some files were not shown because too many files have changed in this diff Show more