Compare commits

...

154 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
199 changed files with 3362 additions and 27837 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

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

59
Godeps/Godeps.json generated
View file

@ -1,59 +0,0 @@
{
"ImportPath": "github.com/codeskyblue/gohttpserver",
"GoVersion": "go1.6",
"GodepVersion": "v74",
"Deps": [
{
"ImportPath": "github.com/DHowett/go-plist",
"Rev": "f4bf55d2395500aacc17eedcebc5f139336b4312"
},
{
"ImportPath": "github.com/alecthomas/kingpin",
"Comment": "v2.1.3",
"Rev": "aef28d186e59d39ed537473dfce4472108ea1045"
},
{
"ImportPath": "github.com/alecthomas/template",
"Rev": "b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0"
},
{
"ImportPath": "github.com/alecthomas/template/parse",
"Rev": "b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0"
},
{
"ImportPath": "github.com/alecthomas/units",
"Rev": "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a"
},
{
"ImportPath": "github.com/codeskyblue/dockerignore",
"Rev": "de82dee623d9207f906d327172149cba50427a88"
},
{
"ImportPath": "github.com/go-yaml/yaml",
"Rev": "e4d366fc3c7938e2958e662b4258c7a89e1f0e3e"
},
{
"ImportPath": "github.com/goji/httpauth",
"Rev": "2da839ab0f4df05a6db5eb277995589dadbd4fb9"
},
{
"ImportPath": "github.com/gorilla/context",
"Comment": "v1.1-4-gaed02d1",
"Rev": "aed02d124ae4a0e94fea4541c8effd05bf0c8296"
},
{
"ImportPath": "github.com/gorilla/handlers",
"Comment": "v1.1-10-g801d6e3",
"Rev": "801d6e3b008914ee888c9ab9b1b379b9a56fbf44"
},
{
"ImportPath": "github.com/gorilla/mux",
"Comment": "v1.1-15-gd391bea",
"Rev": "d391bea3118c9fc17a88d62c9189bb791255e0ef"
},
{
"ImportPath": "github.com/mash/go-accesslog",
"Rev": "9ba8e13f36087d6cb83d9a9f17f9e8da137d5ee9"
}
]
}

5
Godeps/Readme generated
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

219
README.md
View file

@ -1,19 +1,17 @@
# gohttpserver
[![Build Status](https://travis-ci.org/codeskyblue/gohttpserver.svg?branch=master)](https://travis-ci.org/codeskyblue/gohttpserver)
This is a fork from [codeskyblue/gohttpserver](https://github.com/codeskyblue/gohttpserver/) without google-analytics.
Make the best HTTP File Server. Better UI, upload support, apple&android install package qrcode generate.
- Goal: Make the best HTTP File Server.
- Features: Human-friendly UI, file uploading support, direct QR-code generation for Apple & Android install package.
[Demo site](https://gohttpserver.herokuapp.com/)
- 目标: 做最好的HTTP文件服务器
- 功能: 人性化的UI体验文件的上传支持安卓和苹果安装包的二维码直接生成。
**Binary** can be download from [github releases](https://github.com/codeskyblue/gohttpserver/releases/)
## Notes
If using go1.5, ensure you set GO15VENDOREXPERIMENT=1
Upload size now limited to 1G
## Requirements
Tested with go-1.16
## Screenshots
![screen](testdata/filetypes/gohttpserver.gif)
@ -24,7 +22,7 @@ Upload size now limited to 1G
1. [x] All assets package to Standalone binary
1. [x] Different file type different icon
1. [x] Support show or hide hidden files
1. [x] Upload support (for security reason, you need enabled it by option `--upload`)
1. [x] Upload support (auth by token or session)
1. [x] README.md preview
1. [x] HTTP Basic Auth
1. [x] Partial reload pages when directory change
@ -47,9 +45,15 @@ Upload size now limited to 1G
1. [ ] Support sort by size or modified time
1. [x] Add version info into index page
1. [ ] Add api `/-/info/some.(apk|ipa)` to get detail info
1. [x] Add api `/-/apk/info/some.apk` to get android package info
1. [x] Auto tag version
1. [x] Custom title support
1. [x] Support setting from conf file
1. [x] Quick copy download link
1. [x] Show folder size
1. [x] Create folder
1. [x] Skip delete confirm when alt pressed
1. [x] Support unzip zip file when upload(with form: unzip=true)
## Installation
```
@ -57,23 +61,107 @@ go get -v github.com/codeskyblue/gohttpserver
cd $GOPATH/src/github.com/codeskyblue/gohttpserver
go build && ./gohttpserver
```
## Usage
Listen port 8000 on all interface, and enable upload
Listen on port 8000 of all interfaces, and enable file uploading.
```
./gohttpserver -r ./ --addr :8000 --upload
./gohttpserver -r ./ --port 8000 --upload
```
Use command `gohttpserver --help` to see more usage.
## Docker Usage
share current directory
```bash
docker run -it --rm -p 8000:8000 -v $PWD:/app/public --name gohttpserver registry.hhhammer.de/gohttpserver
```
Share current directory with http basic auth
```bash
docker run -it --rm -p 8000:8000 -v $PWD:/app/public --name gohttpserver \
registry.hhhammer.de/gohttpserver \
--auth-type http --auth-http username:password
```
Share current directory with openid auth. (Works only in netease company.)
```bash
docker run -it --rm -p 8000:8000 -v $PWD:/app/public --name gohttpserver \
registry.hhhammer.de/gohttpserver \
--auth-type openid
```
To build image yourself, please change the PWD to the root of this repo.
```bash
cd gohttpserver/
docker build -t registry.hhhhammer.de/gohttpserver -f docker/Dockerfile .
```
## Authentication options
- Enable basic http authentication
```sh
$ gohttpserver --auth-type http --auth-http username:password
```
- Use openid auth
```sh
$ gohttpserver --auth-type openid --auth-openid https://login.example-hostname.com/openid/
```
- Use oauth2-proxy with
```sh
$ gohttpserver --auth-type oauth2-proxy
```
You can configure to let a http reverse proxy handling authentication.
When using oauth2-proxy, the backend will use identification info from request headers `X-Auth-Request-Email` as userId and `X-Auth-Request-Fullname` as user's display name.
Please config your oauth2 reverse proxy yourself.
More about [oauth2-proxy](https://github.com/oauth2-proxy/oauth2-proxy).
All required headers list as following.
|header|value|
|---|---|
|X-Auth-Request-Email| userId |
|X-Auth-Request-Fullname| user's display name(urlencoded) |
|X-Auth-Request-User| user's nickname (mostly email prefix) |
- Enable upload
```sh
$ gohttpserver --upload
```
- Enable delete and Create folder
```sh
$ gohttpserver --delete
```
## Advanced usage
Support update access rule if there is a file named `.ghs.yml` under directory. `.ghs.yml` example
Add access rule by creating a `.ghs.yml` file under a sub-directory. An example:
```yaml
---
upload: false
delete: false
users:
- email: "codeskyblue@codeskyblue.com"
delete: true
upload: true
token: 4567gf8asydhf293r23r
```
For example, if there is such file under directory `foo`, directory `foo` can not be uploaded, while `bar` can.
In this case, if openid auth is enabled and user "codeskyblue@codeskyblue.com" has logged in, he/she can delete/upload files under the directory where the `.ghs.yml` file exits.
`token` is used for upload. see [upload with curl](#upload-with-curl)
For example, in the following directory hierarchy, users can delete/uploade files in directory `foo`, but he/she cannot do this in directory `bar`.
```
root -
@ -84,62 +172,114 @@ root -
`-- hello.txt
```
Use config file. specfied with `--conf`, see [example config.yml](testdata/config.yml). Note that command line option can overwrite conf in `config.yml`
User can specify config file name with `--conf`, see [example config.yml](testdata/config.yml).
To specify which files is hidden and which file is visible, add the following lines to `.ghs.yml`
```yaml
accessTables:
- regex: block.file
allow: false
- regex: visual.file
allow: true
```
### ipa plist proxy
This is used for server which not https enabled. default use <https://plistproxy.herokuapp.com/plist>
This is used for server on which https is enabled. default use <https://plistproxy.herokuapp.com/plist>
```
./gohttpserver --plistproxy=https://someproxyhost.com/
```
Proxy web site should have ability
Test if proxy works:
```sh
$ http POST https://proxyhost.com/plist < app.plist
$ http POST https://someproxyhost.com/plist < app.plist
{
"key": "18f99211"
}
$ http GET https://proxyhost.com/plist/18f99211
$ http GET https://someproxyhost.com/plist/18f99211
# show the app.plist content
```
If your ghs running behide nginx server and have https configed. plistproxy will be disabled automaticly.
### Upload with CURL
For example, upload a file named `foo.txt` to directory `somedir`
PS: max upload size limited to 1G (hard coded)
```sh
$ curl -F file=@foo.txt localhost:8000/somedir
{"destination":"somedir/foo.txt","success":true}
# upload with token
$ curl -F file=@foo.txt -F token=12312jlkjafs localhost:8000/somedir
{"destination":"somedir/foo.txt","success":true}
# upload and change filename
$ curl -F file=@foo.txt -F filename=hi.txt localhost:8000/somedir
{"destination":"somedir/hi.txt","success":true}
```
Upload zip file and unzip it (zip file will be delete when finished unzip)
```
$ curl -F file=@pkg.zip -F unzip=true localhost:8000/somedir
{"success": true}
```
Note: `\/:*<>|` are not allowed in filenames.
### Deploy with nginx
Recommended configuration, assume your gohttpserver listening on `127.0.0.1:8200`
```
server {
listen 80;
server_name your-domain-name.com;
location / {
proxy_pass http://127.0.0.1:8200; # here need to change
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 0; # disable upload limit
}
}
```
gohttpserver should started with `--xheaders` argument when behide nginx.
Refs: <http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size>
## FAQ
- [How to generate self signed certificate with openssl](http://stackoverflow.com/questions/10175812/how-to-create-a-self-signed-certificate-with-openssl)
### How the search works
The search algorithm follow the search engine google. keywords are seperated with space, words with prefix `-` will be excluded.
### How the query is formated
The search query follows common format rules just like Google. Keywords are seperated with space(s), keywords with prefix `-` will be excluded in search results.
1. `hello world` means must contains `hello` and `world`
1. `hello -world` means must contains `hello` but not contains `world`
## Developer Guide
Depdencies are managed by godep
Depdencies are managed by [govendor](https://github.com/kardianos/govendor)
```sh
go get -v github.com/tools/godep
go get github.com/jteeuwen/go-bindata/...
go get github.com/elazarl/go-bindata-assetfs/...
```
1. Build develop version. **assets** directory must exists
Theme are all defined in [res/themes](res/themes) directory. Now only two, black and green.
```sh
go build
./gohttpserver
```
2. Build single binary release
## How to build single binary release
```sh
go-bindata-assetfs -tags bindata res/...
go build -tags bindata
```
```sh
go generate .
go build -tags vfs
```
Theme are defined in [assets/themes](assets/themes) directory. Now only two themes are available, "black" and "green".
That's all. ^_^
## Reference Web sites
@ -150,14 +290,17 @@ That's all. ^_^
* Markdown CSS <https://github.com/sindresorhus/github-markdown-css>
* Upload support <http://www.dropzonejs.com/>
* ScrollUp <https://markgoodyear.com/2013/01/scrollup-jquery-plugin/>
* Clipboard <https://clipboardjs.com/>
* Underscore <http://underscorejs.org/>
**Go Libraries**
* <https://github.com/elazarl/go-bindata-assetfs>
* [vfsgen](https://github.com/shurcooL/vfsgen)
* [go-bindata-assetfs](https://github.com/elazarl/go-bindata-assetfs) Not using now
* <http://www.gorillatoolkit.org/pkg/handlers>
## History
The first version is <https://github.com/codeskyblue/gohttp>
The old version is hosted at <https://github.com/codeskyblue/gohttp>
## LICENSE
This project is under license [MIT](LICENSE)
This project is licensed under [MIT](LICENSE).

View file

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

View file

@ -24,3 +24,11 @@ div.dropzone {
.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

View file

Before

Width:  |  Height:  |  Size: 698 B

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();
});
});

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

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

9
assets_dev.go Normal file
View file

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

23
assets_generate.go Normal file
View file

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

View file

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

14
docker/Dockerfile Normal file
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

@ -3,6 +3,9 @@ package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"html/template"
"io"
"io/ioutil"
"log"
@ -13,28 +16,44 @@ import (
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"regexp"
"github.com/go-yaml/yaml"
"github.com/gorilla/mux"
"github.com/shogo82148/androidbinary/apk"
)
const YAMLCONF = ".ghs.yml"
type ApkInfo struct {
PackageName string `json:"packageName"`
MainActivity string `json:"mainActivity"`
Version struct {
Code int `json:"code"`
Name string `json:"name"`
} `json:"version"`
}
type IndexFileItem struct {
Path string
Info os.FileInfo
}
type HTTPStaticServer struct {
Root string
Upload bool
Delete bool
Title string
Theme string
PlistProxy string
GoogleTrackerId string
Root string
Upload bool
Delete bool
Title string
Theme string
PlistProxy string
AuthType string
indexes []IndexFileItem
m *mux.Router
bufPool sync.Pool // use sync.Pool caching buf to reduce gc ratio
}
func NewHTTPStaticServer(root string) *HTTPStaticServer {
@ -51,6 +70,9 @@ func NewHTTPStaticServer(root string) *HTTPStaticServer {
Root: root,
Theme: "black",
m: m,
bufPool: sync.Pool{
New: func() interface{} { return make([]byte, 32*1024) },
},
}
go func() {
@ -65,17 +87,12 @@ func NewHTTPStaticServer(root string) *HTTPStaticServer {
}
}()
m.HandleFunc("/-/status", s.hStatus)
m.HandleFunc("/-/zip/{path:.*}", s.hZip)
m.HandleFunc("/-/unzip/{zip_path:.*}/-/{path:.*}", s.hUnzip)
m.HandleFunc("/-/json/{path:.*}", s.hJSONList)
// routers for Apple *.ipa
m.HandleFunc("/-/ipa/plist/{path:.*}", s.hPlist)
m.HandleFunc("/-/ipa/link/{path:.*}", s.hIpaLink)
// TODO: /ipa/info
m.HandleFunc("/{path:.*}", s.hIndex).Methods("GET")
m.HandleFunc("/{path:.*}", s.hUpload).Methods("POST")
m.HandleFunc("/{path:.*}", s.hIndex).Methods("GET", "HEAD")
m.HandleFunc("/{path:.*}", s.hUploadOrMkdir).Methods("POST")
m.HandleFunc("/{path:.*}", s.hDelete).Methods("DELETE")
return s
}
@ -87,11 +104,35 @@ func (s *HTTPStaticServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (s *HTTPStaticServer) hIndex(w http.ResponseWriter, r *http.Request) {
path := mux.Vars(r)["path"]
relPath := filepath.Join(s.Root, path)
if r.FormValue("json") == "true" {
s.hJSONList(w, r)
return
}
finfo, err := os.Stat(relPath)
if err == nil && finfo.IsDir() {
tmpl.ExecuteTemplate(w, "index", s)
if r.FormValue("op") == "info" {
s.hInfo(w, r)
return
}
if r.FormValue("op") == "archive" {
s.hZip(w, r)
return
}
log.Println("GET", path, relPath)
if r.FormValue("raw") == "false" || isDir(relPath) {
if r.Method == "HEAD" {
return
}
renderHTML(w, "index.html", s)
} else {
if filepath.Base(path) == YAMLCONF {
auth := s.readAccessConf(path)
if !auth.Delete {
http.Error(w, "Security warning, not allowed to read", http.StatusForbidden)
return
}
}
if r.FormValue("download") == "true" {
w.Header().Set("Content-Disposition", "attachment; filename="+strconv.Quote(filepath.Base(path)))
}
@ -99,22 +140,20 @@ func (s *HTTPStaticServer) hIndex(w http.ResponseWriter, r *http.Request) {
}
}
func (s *HTTPStaticServer) hStatus(w http.ResponseWriter, r *http.Request) {
data, _ := json.MarshalIndent(s, "", " ")
w.Header().Set("Content-Type", "application/json")
w.Write(data)
}
func (s *HTTPStaticServer) hDelete(w http.ResponseWriter, req *http.Request) {
// only can delete file now
path := mux.Vars(req)["path"]
func (s *HTTPStaticServer) hMkdir(w http.ResponseWriter, req *http.Request) {
path := filepath.Dir(mux.Vars(req)["path"])
auth := s.readAccessConf(path)
log.Printf("%#v", auth)
if !auth.Delete {
http.Error(w, "Delete forbidden", http.StatusForbidden)
if !auth.canDelete(req) {
http.Error(w, "Mkdir forbidden", http.StatusForbidden)
return
}
err := os.Remove(filepath.Join(s.Root, path))
name := filepath.Base(mux.Vars(req)["path"])
if err := checkFilename(name); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
err := os.Mkdir(filepath.Join(s.Root, path, name), 0755)
if err != nil {
http.Error(w, err.Error(), 500)
return
@ -122,48 +161,191 @@ func (s *HTTPStaticServer) hDelete(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("Success"))
}
func (s *HTTPStaticServer) hUpload(w http.ResponseWriter, req *http.Request) {
func (s *HTTPStaticServer) hDelete(w http.ResponseWriter, req *http.Request) {
path := mux.Vars(req)["path"]
path = filepath.Clean(path) // for safe reason, prevent path contain ..
auth := s.readAccessConf(path)
if !auth.canDelete(req) {
http.Error(w, "Delete forbidden", http.StatusForbidden)
return
}
// TODO: path safe check
err := os.RemoveAll(filepath.Join(s.Root, path))
if err != nil {
pathErr, ok := err.(*os.PathError)
if ok {
http.Error(w, pathErr.Op+" "+path+": "+pathErr.Err.Error(), 500)
} else {
http.Error(w, err.Error(), 500)
}
return
}
w.Write([]byte("Success"))
}
func (s *HTTPStaticServer) hUploadOrMkdir(w http.ResponseWriter, req *http.Request) {
path := mux.Vars(req)["path"]
dirpath := filepath.Join(s.Root, path)
// check auth
auth := s.readAccessConf(path)
if !auth.Upload {
if !auth.canUpload(req) {
http.Error(w, "Upload forbidden", http.StatusForbidden)
return
}
err := req.ParseMultipartForm(1 << 30) // max memory 1G
file, header, err := req.FormFile("file")
if _, err := os.Stat(dirpath); os.IsNotExist(err) {
if err := os.MkdirAll(dirpath, os.ModePerm); err != nil {
log.Println("Create directory:", err)
http.Error(w, "Directory create "+err.Error(), http.StatusInternalServerError)
return
}
}
if file == nil { // only mkdir
w.Header().Set("Content-Type", "application/json;charset=utf-8")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"destination": dirpath,
})
return
}
if err != nil {
log.Println("Parse form file:", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if len(req.MultipartForm.File["file"]) == 0 {
http.Error(w, "Need multipart file", http.StatusInternalServerError)
defer func() {
file.Close()
req.MultipartForm.RemoveAll() // Seen from go source code, req.MultipartForm not nil after call FormFile(..)
}()
filename := req.FormValue("filename")
if filename == "" {
filename = header.Filename
}
if err := checkFilename(filename); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
dirpath := filepath.Join(s.Root, path)
dstPath := filepath.Join(dirpath, filename)
for _, mfile := range req.MultipartForm.File["file"] {
file, err := mfile.Open()
defer file.Close()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
dst, err := os.Create(filepath.Join(dirpath, mfile.Filename)) // BUG(ssx): There is a leak here
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer dst.Close()
if _, err := io.Copy(dst, file); err != nil {
log.Println("Handle upload file:", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Large file (>32MB) will store in tmp directory
// The quickest operation is call os.Move instead of os.Copy
// Note: it seems not working well, os.Rename might be failed
var copyErr error
// if osFile, ok := file.(*os.File); ok && fileExists(osFile.Name()) {
// tmpUploadPath := osFile.Name()
// osFile.Close() // Windows can not rename opened file
// log.Printf("Move %s -> %s", tmpUploadPath, dstPath)
// copyErr = os.Rename(tmpUploadPath, dstPath)
// } else {
dst, err := os.Create(dstPath)
if err != nil {
log.Println("Create file:", err)
http.Error(w, "File create "+err.Error(), http.StatusInternalServerError)
return
}
w.Write([]byte("Upload success"))
// Note: very large size file might cause poor performance
// _, copyErr = io.Copy(dst, file)
buf := s.bufPool.Get().([]byte)
defer s.bufPool.Put(buf)
_, copyErr = io.CopyBuffer(dst, file, buf)
dst.Close()
// }
if copyErr != nil {
log.Println("Handle upload file:", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json;charset=utf-8")
if req.FormValue("unzip") == "true" {
err = unzipFile(dstPath, dirpath)
os.Remove(dstPath)
message := "success"
if err != nil {
message = err.Error()
}
json.NewEncoder(w).Encode(map[string]interface{}{
"success": err == nil,
"description": message,
})
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"destination": dstPath,
})
}
type FileJSONInfo struct {
Name string `json:"name"`
Type string `json:"type"`
Size int64 `json:"size"`
Path string `json:"path"`
ModTime int64 `json:"mtime"`
Extra interface{} `json:"extra,omitempty"`
}
// path should be absolute
func parseApkInfo(path string) (ai *ApkInfo) {
defer func() {
if err := recover(); err != nil {
log.Println("parse-apk-info panic:", err)
}
}()
apkf, err := apk.OpenFile(path)
if err != nil {
return
}
ai = &ApkInfo{}
ai.MainActivity, _ = apkf.MainActivity()
ai.PackageName = apkf.PackageName()
ai.Version.Code = apkf.Manifest().VersionCode
ai.Version.Name = apkf.Manifest().VersionName
return
}
func (s *HTTPStaticServer) hInfo(w http.ResponseWriter, r *http.Request) {
path := mux.Vars(r)["path"]
relPath := filepath.Join(s.Root, path)
fi, err := os.Stat(relPath)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
fji := &FileJSONInfo{
Name: fi.Name(),
Size: fi.Size(),
Path: path,
ModTime: fi.ModTime().UnixNano() / 1e6,
}
ext := filepath.Ext(path)
switch ext {
case ".md":
fji.Type = "markdown"
case ".apk":
fji.Type = "apk"
fji.Extra = parseApkInfo(relPath)
case "":
fji.Type = "dir"
default:
fji.Type = "text"
}
data, _ := json.Marshal(fji)
w.Header().Set("Content-Type", "application/json")
w.Write(data)
}
func (s *HTTPStaticServer) hZip(w http.ResponseWriter, r *http.Request) {
@ -185,13 +367,9 @@ func (s *HTTPStaticServer) hUnzip(w http.ResponseWriter, r *http.Request) {
}
}
func genURLStr(r *http.Request, path string) *url.URL {
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
func combineURL(r *http.Request, path string) *url.URL {
return &url.URL{
Scheme: scheme,
Scheme: r.URL.Scheme,
Host: r.Host,
Path: path,
}
@ -230,9 +408,11 @@ func (s *HTTPStaticServer) hPlist(w http.ResponseWriter, r *http.Request) {
func (s *HTTPStaticServer) hIpaLink(w http.ResponseWriter, r *http.Request) {
path := mux.Vars(r)["path"]
plistUrl := genURLStr(r, "/-/ipa/plist/"+path).String()
if r.TLS == nil {
// send plist to plistproxy and get a https link
var plistUrl string
if r.URL.Scheme == "https" {
plistUrl = combineURL(r, "/-/ipa/plist/"+path).String()
} else if s.PlistProxy != "" {
httpPlistLink := "http://" + r.Host + "/-/ipa/plist/" + path
url, err := s.genPlistLink(httpPlistLink)
if err != nil {
@ -240,17 +420,17 @@ func (s *HTTPStaticServer) hIpaLink(w http.ResponseWriter, r *http.Request) {
return
}
plistUrl = url
//plistUrl = strings.TrimSuffix(s.PlistProxy, "/") + "/" + r.Host + "/-/ipa/plist/" + path
} else {
http.Error(w, "500: Server should be https:// or provide valid plistproxy", 500)
return
}
w.Header().Set("Content-Type", "text/html")
tmpl.ExecuteTemplate(w, "ipa-install", map[string]string{
log.Println("PlistURL:", plistUrl)
renderHTML(w, "ipa-install.html", map[string]string{
"Name": filepath.Base(path),
"PlistLink": plistUrl,
})
// w.Write([]byte(fmt.Sprintf(
// `<a href='itms-services://?action=download-manifest&url=%s'>Click this link to install</a>`,
// plistUrl)))
}
func (s *HTTPStaticServer) genPlistLink(httpPlistLink string) (plistUrl string, err error) {
@ -286,23 +466,111 @@ func (s *HTTPStaticServer) hFileOrDirectory(w http.ResponseWriter, r *http.Reque
http.ServeFile(w, r, filepath.Join(s.Root, path))
}
type ListResponse struct {
type HTTPFileInfo struct {
Name string `json:"name"`
Path string `json:"path"`
Type string `json:"type"`
Size string `json:"size"`
Size int64 `json:"size"`
ModTime int64 `json:"mtime"`
}
type AccessTable struct {
Regex string `yaml:"regex"`
Allow bool `yaml:"allow"`
}
type UserControl struct {
Email string
// Access bool
Upload bool
Delete bool
Token string
}
type AccessConf struct {
Upload bool `yaml:"upload" json:"upload"`
Delete bool `yaml:"delete" json:"delete"`
Upload bool `yaml:"upload" json:"upload"`
Delete bool `yaml:"delete" json:"delete"`
Users []UserControl `yaml:"users" json:"users"`
AccessTables []AccessTable `yaml:"accessTables"`
}
var reCache = make(map[string]*regexp.Regexp)
func (c *AccessConf) canAccess(fileName string) bool {
for _, table := range c.AccessTables {
pattern, ok := reCache[table.Regex]
if !ok {
pattern, _ = regexp.Compile(table.Regex)
reCache[table.Regex] = pattern
}
// skip wrong format regex
if pattern == nil {
continue
}
if pattern.MatchString(fileName) {
return table.Allow
}
}
return true
}
func (c *AccessConf) canDelete(r *http.Request) bool {
session, err := store.Get(r, defaultSessionName)
if err != nil {
return c.Delete
}
val := session.Values["user"]
if val == nil {
return c.Delete
}
userInfo := val.(*UserInfo)
for _, rule := range c.Users {
if rule.Email == userInfo.Email {
return rule.Delete
}
}
return c.Delete
}
func (c *AccessConf) canUploadByToken(token string) bool {
for _, rule := range c.Users {
if rule.Token == token {
return rule.Upload
}
}
return c.Upload
}
func (c *AccessConf) canUpload(r *http.Request) bool {
token := r.FormValue("token")
if token != "" {
return c.canUploadByToken(token)
}
session, err := store.Get(r, defaultSessionName)
if err != nil {
return c.Upload
}
val := session.Values["user"]
if val == nil {
return c.Upload
}
userInfo := val.(*UserInfo)
for _, rule := range c.Users {
if rule.Email == userInfo.Email {
return rule.Upload
}
}
return c.Upload
}
func (s *HTTPStaticServer) hJSONList(w http.ResponseWriter, r *http.Request) {
requestPath := mux.Vars(r)["path"]
localPath := filepath.Join(s.Root, requestPath)
search := r.FormValue("search")
auth := s.readAccessConf(requestPath)
auth.Upload = auth.canUpload(r)
auth.Delete = auth.canDelete(r)
// path string -> info os.FileInfo
fileInfoMap := make(map[string]os.FileInfo, 0)
@ -329,9 +597,12 @@ func (s *HTTPStaticServer) hJSONList(w http.ResponseWriter, r *http.Request) {
}
// turn file list -> json
lrs := make([]ListResponse, 0)
lrs := make([]HTTPFileInfo, 0)
for path, info := range fileInfoMap {
lr := ListResponse{
if !auth.canAccess(info.Name()) {
continue
}
lr := HTTPFileInfo{
Name: info.Name(),
Path: path,
ModTime: info.ModTime().UnixNano() / 1e6,
@ -348,39 +619,60 @@ func (s *HTTPStaticServer) hJSONList(w http.ResponseWriter, r *http.Request) {
lr.Name = name
lr.Path = filepath.Join(filepath.Dir(path), name)
lr.Type = "dir"
lr.Size = "-"
lr.Size = s.historyDirSize(lr.Path)
} else {
lr.Type = "file"
lr.Size = formatSize(info)
lr.Size = info.Size() // formatSize(info)
}
lrs = append(lrs, lr)
}
data, _ := json.Marshal(map[string]interface{}{
"files": lrs,
"auth": s.readAccessConf(requestPath),
"auth": auth,
})
w.Header().Set("Content-Type", "application/json")
w.Write(data)
}
var dirSizeMap = make(map[string]int64)
func (s *HTTPStaticServer) makeIndex() error {
var indexes = make([]IndexFileItem, 0)
var err = filepath.Walk(s.Root, func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Printf("WARN: Visit path: %s error: %v", strconv.Quote(path), err)
return filepath.SkipDir
// return err
}
if info.IsDir() {
return nil
}
if filepath.IsAbs(path) {
path, _ = filepath.Rel(s.Root, path)
}
path, _ = filepath.Rel(s.Root, path)
path = filepath.ToSlash(path)
indexes = append(indexes, IndexFileItem{path, info})
return nil
})
s.indexes = indexes
dirSizeMap = make(map[string]int64)
return err
}
func (s *HTTPStaticServer) historyDirSize(dir string) int64 {
var size int64
if size, ok := dirSizeMap[dir]; ok {
return size
}
for _, fitem := range s.indexes {
if filepath.HasPrefix(fitem.Path, dir) {
size += fitem.Info.Size()
}
}
dirSizeMap[dir] = size
return size
}
func (s *HTTPStaticServer) findIndex(text string) []IndexFileItem {
ret := make([]IndexFileItem, 0)
for _, item := range s.indexes {
@ -410,16 +702,23 @@ func (s *HTTPStaticServer) findIndex(text string) []IndexFileItem {
func (s *HTTPStaticServer) defaultAccessConf() AccessConf {
return AccessConf{
Upload: s.Upload,
Delete: s.Delete,
}
}
func (s *HTTPStaticServer) readAccessConf(requestPath string) (ac AccessConf) {
ac = s.defaultAccessConf()
requestPath = filepath.Clean(requestPath)
if requestPath == "/" || requestPath == "" || requestPath == "." {
ac = s.defaultAccessConf()
} else {
parentPath := filepath.Dir(requestPath)
ac = s.readAccessConf(parentPath)
}
relPath := filepath.Join(s.Root, requestPath)
if isFile(relPath) {
relPath = filepath.Dir(relPath)
}
cfgFile := filepath.Join(relPath, ".ghs.yml")
cfgFile := filepath.Join(relPath, YAMLCONF)
data, err := ioutil.ReadFile(cfgFile)
if err != nil {
if os.IsNotExist(err) {
@ -456,3 +755,73 @@ func isFile(path string) bool {
info, err := os.Stat(path)
return err == nil && info.Mode().IsRegular()
}
func isDir(path string) bool {
info, err := os.Stat(path)
return err == nil && info.Mode().IsDir()
}
func assetsContent(name string) string {
fd, err := Assets.Open(name)
if err != nil {
panic(err)
}
data, err := ioutil.ReadAll(fd)
if err != nil {
panic(err)
}
return string(data)
}
// TODO: I need to read more abouthtml/template
var (
funcMap template.FuncMap
)
func init() {
funcMap = template.FuncMap{
"title": strings.Title,
"urlhash": func(path string) string {
httpFile, err := Assets.Open(path)
if err != nil {
return path + "#no-such-file"
}
info, err := httpFile.Stat()
if err != nil {
return path + "#stat-error"
}
return fmt.Sprintf("%s?t=%d", path, info.ModTime().Unix())
},
}
}
var (
_tmpls = make(map[string]*template.Template)
)
func executeTemplate(w http.ResponseWriter, name string, v interface{}) {
if t, ok := _tmpls[name]; ok {
t.Execute(w, v)
return
}
t := template.Must(template.New(name).Funcs(funcMap).Delims("[[", "]]").Parse(assetsContent(name)))
_tmpls[name] = t
t.Execute(w, v)
}
func renderHTML(w http.ResponseWriter, name string, v interface{}) {
if _, ok := Assets.(http.Dir); ok {
log.Println("Hot load", name)
t := template.Must(template.New(name).Funcs(funcMap).Delims("[[", "]]").Parse(assetsContent(name)))
t.Execute(w, v)
} else {
executeTemplate(w, name, v)
}
}
func checkFilename(name string) error {
if strings.ContainsAny(name, "\\/:*<>|") {
return errors.New("Name should not contains \\/:*<>|")
}
return nil
}

3
ipa.go
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) {

96
main.go
View file

@ -6,6 +6,7 @@ import (
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
"os"
@ -15,40 +16,49 @@ import (
"text/template"
"github.com/alecthomas/kingpin"
accesslog "github.com/codeskyblue/go-accesslog"
"github.com/go-yaml/yaml"
"github.com/goji/httpauth"
"github.com/gorilla/handlers"
accesslog "github.com/mash/go-accesslog"
_ "github.com/shurcooL/vfsgen"
)
type Configure struct {
Conf *os.File `yaml:"-"`
Addr string `yaml:"addr"`
Root string `yaml:"root"`
HttpAuth string `yaml:"httpauth"`
Cert string `yaml:"cert"`
Key string `yaml:"key"`
Cors bool `yaml:"cors"`
Theme string `yaml:"theme"`
XHeaders bool `yaml:"xheaders"`
Upload bool `yaml:"upload"`
PlistProxy string `yaml:"plistproxy"`
Title string `yaml:"title"`
Debug bool `yaml:"debug"`
GoogleTrackerId string `yaml:"google-tracker-id"`
Conf *os.File `yaml:"-"`
Addr string `yaml:"addr"`
Port int `yaml:"port"`
Root string `yaml:"root"`
HTTPAuth string `yaml:"httpauth"`
Cert string `yaml:"cert"`
Key string `yaml:"key"`
Cors bool `yaml:"cors"`
Theme string `yaml:"theme"`
XHeaders bool `yaml:"xheaders"`
Upload bool `yaml:"upload"`
Delete bool `yaml:"delete"`
PlistProxy string `yaml:"plistproxy"`
Title string `yaml:"title"`
Debug bool `yaml:"debug"`
Auth struct {
Type string `yaml:"type"` // openid|http|github
OpenID string `yaml:"openid"`
HTTP string `yaml:"http"`
ID string `yaml:"id"` // for oauth2
Secret string `yaml:"secret"` // for oauth2
} `yaml:"auth"`
}
type logger struct {
}
type httpLogger struct{}
func (l logger) Log(record accesslog.LogRecord) {
func (l httpLogger) Log(record accesslog.LogRecord) {
log.Printf("%s - %s %d %s", record.Ip, record.Method, record.Status, record.Uri)
}
var (
defaultPlistProxy = "https://plistproxy.herokuapp.com/plist"
defaultOpenID = "https://login.netease.com/openid"
gcfg = Configure{}
l = logger{}
logger = httpLogger{}
VERSION = "unknown"
BUILDTIME = "unknown time"
@ -79,28 +89,32 @@ func versionMessage() string {
func parseFlags() error {
// initial default conf
gcfg.Root = "./"
gcfg.Addr = ":8000"
gcfg.Port = 8000
gcfg.Addr = ""
gcfg.Theme = "black"
gcfg.PlistProxy = defaultPlistProxy
gcfg.GoogleTrackerId = "UA-81205425-2"
gcfg.Auth.OpenID = defaultOpenID
gcfg.Title = "Go HTTP File Server"
kingpin.HelpFlag.Short('h')
kingpin.Version(versionMessage())
kingpin.Flag("conf", "config file path, yaml format").FileVar(&gcfg.Conf)
kingpin.Flag("root", "root directory, default ./").Short('r').StringVar(&gcfg.Root)
kingpin.Flag("addr", "listen address, default :8000").Short('a').StringVar(&gcfg.Addr)
kingpin.Flag("port", "listen port, default 8000").IntVar(&gcfg.Port)
kingpin.Flag("addr", "listen address, eg 127.0.0.1:8000").Short('a').StringVar(&gcfg.Addr)
kingpin.Flag("cert", "tls cert.pem path").StringVar(&gcfg.Cert)
kingpin.Flag("key", "tls key.pem path").StringVar(&gcfg.Key)
kingpin.Flag("httpauth", "HTTP basic auth (ex: user:pass)").StringVar(&gcfg.HttpAuth)
kingpin.Flag("auth-type", "Auth type <http|openid>").StringVar(&gcfg.Auth.Type)
kingpin.Flag("auth-http", "HTTP basic auth (ex: user:pass)").StringVar(&gcfg.Auth.HTTP)
kingpin.Flag("auth-openid", "OpenID auth identity url").StringVar(&gcfg.Auth.OpenID)
kingpin.Flag("theme", "web theme, one of <black|green>").StringVar(&gcfg.Theme)
kingpin.Flag("upload", "enable upload support").BoolVar(&gcfg.Upload)
kingpin.Flag("delete", "enable delete support").BoolVar(&gcfg.Delete)
kingpin.Flag("xheaders", "used when behide nginx").BoolVar(&gcfg.XHeaders)
kingpin.Flag("cors", "enable cross-site HTTP request").BoolVar(&gcfg.Cors)
kingpin.Flag("debug", "enable debug mode").BoolVar(&gcfg.Debug)
kingpin.Flag("plistproxy", "plist proxy when server is not https").Short('p').StringVar(&gcfg.PlistProxy)
kingpin.Flag("title", "server title").StringVar(&gcfg.Title)
kingpin.Flag("google-tracker-id", "set to empty to disable it").StringVar(&gcfg.GoogleTrackerId)
kingpin.Parse() // first parse conf
@ -125,12 +139,14 @@ func main() {
data, _ := yaml.Marshal(gcfg)
fmt.Printf("--- config ---\n%s\n", string(data))
}
log.SetFlags(log.Lshortfile | log.LstdFlags)
ss := NewHTTPStaticServer(gcfg.Root)
ss.Theme = gcfg.Theme
ss.Title = gcfg.Title
ss.GoogleTrackerId = gcfg.GoogleTrackerId
ss.Upload = gcfg.Upload
ss.Delete = gcfg.Delete
ss.AuthType = gcfg.Auth.Type
if gcfg.PlistProxy != "" {
u, err := url.Parse(gcfg.PlistProxy)
@ -140,17 +156,30 @@ func main() {
u.Scheme = "https"
ss.PlistProxy = u.String()
}
if ss.PlistProxy != "" {
log.Printf("plistproxy: %s", strconv.Quote(ss.PlistProxy))
}
var hdlr http.Handler = ss
hdlr = accesslog.NewLoggingHandler(hdlr, l)
hdlr = accesslog.NewLoggingHandler(hdlr, logger)
// HTTP Basic Authentication
userpass := strings.SplitN(gcfg.HttpAuth, ":", 2)
if len(userpass) == 2 {
user, pass := userpass[0], userpass[1]
hdlr = httpauth.SimpleBasicAuth(user, pass)(hdlr)
userpass := strings.SplitN(gcfg.Auth.HTTP, ":", 2)
switch gcfg.Auth.Type {
case "http":
if len(userpass) == 2 {
user, pass := userpass[0], userpass[1]
hdlr = httpauth.SimpleBasicAuth(user, pass)(hdlr)
}
case "openid":
handleOpenID(gcfg.Auth.OpenID, false) // FIXME(ssx): set secure default to false
// case "github":
// handleOAuth2ID(gcfg.Auth.Type, gcfg.Auth.ID, gcfg.Auth.Secret) // FIXME(ssx): set secure default to false
case "oauth2-proxy":
handleOauth2()
}
// CORS
if gcfg.Cors {
hdlr = handlers.CORS()(hdlr)
@ -160,6 +189,7 @@ func main() {
}
http.Handle("/", hdlr)
http.Handle("/-/assets/", http.StripPrefix("/-/assets/", http.FileServer(Assets)))
http.HandleFunc("/-/sysinfo", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
data, _ := json.Marshal(map[string]interface{}{
@ -168,10 +198,14 @@ func main() {
w.Write(data)
})
if gcfg.Addr == "" {
gcfg.Addr = fmt.Sprintf(":%d", gcfg.Port)
}
if !strings.Contains(gcfg.Addr, ":") {
gcfg.Addr = ":" + gcfg.Addr
}
log.Printf("listening on %s\n", strconv.Quote(gcfg.Addr))
_, port, _ := net.SplitHostPort(gcfg.Addr)
log.Printf("listening on %s, local address http://%s:%s\n", strconv.Quote(gcfg.Addr), getLocalIP(), port)
var err error
if gcfg.Key != "" && gcfg.Cert != "" {

27
oauth2-proxy.go Normal file
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,219 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>gohttp server</title>
<link rel="shortcut icon" type="image/png" href="/-/res/favicon.png" />
<link rel="stylesheet" type="text/css" href="/-/res/bootstrap-3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/-/res/font-awesome-4.6.3/css/font-awesome.min.css">
<link rel="stylesheet" type="text/css" href="/-/res/css/github-markdown.css">
<link rel="stylesheet" type="text/css" href="/-/res/css/dropzone.css">
<link rel="stylesheet" type="text/css" href="/-/res/css/scrollUp-image.css">
<link rel="stylesheet" type="text/css" href="/-/res/css/style.css">
<link rel="stylesheet" type="text/css" href="/-/res/themes/[[.Theme]].css">
</head>
<body id="app">
<nav class="navbar navbar-default">
<div class="container">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-2">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">[[.Title]]</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-2">
<ul class="nav navbar-nav">
<li class="hidden-xs">
<a href="javascript:void(0)" v-on:click='genQrcode("/", location.origin)'>
View in Phone
<span class="glyphicon glyphicon-qrcode"></span>
</a>
</li>
</ul>
<form class="navbar-form navbar-right">
<div class="input-group">
<input type="text" name="search" class="form-control" placeholder="Search text" v-bind:value="search" autofocus>
<span class="input-group-btn">
<button class="btn btn-default" type="button">
<span class="glyphicon glyphicon-search"></span>
</button>
</span>
</div>
</form>
<ul id="nav-right-bar" class="nav navbar-nav navbar-right">
</ul>
</div>
</div>
</div>
</nav>
<div class="container">
<div class="col-md-12">
<ol class="breadcrumb">
<li>
<a v-on:click='changePath("/", $event)' href="/"><i class="fa fa-home"></i></a>
</li>
<li v-for="bc in breadcrumb.slice(0, breadcrumb.length-1)">
<a v-on:click='changePath(bc.path, $event)' href="{{bc.path}}">{{bc.name}}</a>
</li>
<li v-if="breadcrumb.length >= 1">
{{breadcrumb.slice(-1)[0].name}}
</li>
</ol>
<table class="table table-hover">
<thead>
<tr>
<td colspan=4>
<!-- <button class="btn btn-xs btn-default" v-on:click='toggleHidden()'>
Back <i class="fa" v-bind:class='showHidden ? "fa-eye" : "fa-eye-slash"'></i>
</button> -->
<button class="btn btn-xs btn-default" v-on:click='toggleHidden()'>
Hidden <i class="fa" v-bind:class='showHidden ? "fa-eye" : "fa-eye-slash"'></i>
</button>
<button class="btn btn-xs btn-default" v-if="auth.upload" data-toggle="modal" data-target="#upload-modal">
Upload <i class="fa fa-upload"></i>
</button>
</td>
</tr>
<tr>
<th>Name</th>
<th>Size</th>
<th class="hidden-xs">
<span style="cursor: pointer" v-on:click='mtimeTypeFromNow = !mtimeTypeFromNow'>ModTime</span>
</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="f in computedFiles">
<td>
<a v-on:click='clickFileOrDir(f, $event)' href="/{{f.path}}">
<i style="padding-right: 0.5em" class="fa" v-bind:class='genFileClass(f)'></i> {{f.name}}
</a>
</td>
<td>{{f.size}}</td>
<td class="hidden-xs">{{formatTime(f.mtime)}}</td>
<td style="text-align: left">
<template v-if="f.type == 'dir'">
<a class="btn btn-default btn-xs" href="/-/zip/{{f.path}}">
<span class="hidden-xs">Archive</span> Zip
<span class="glyphicon glyphicon-download-alt"></span>
</a>
</template>
<template v-if="f.type == 'file'">
<a class="btn btn-default btn-xs hidden-xs" href="/{{f.path}}?download=true">
<span class="hidden-xs">Download</span>
<span class="glyphicon glyphicon-download-alt"></span>
</a>
<a class="btn btn-default btn-xs hidden-xs" v-on:click="genQrcode(f.name)" href="javascript:void(0)">
<span v-if="shouldHaveQrcode(f.name)">QRCode</span>
<span class="glyphicon glyphicon-qrcode"></span>
</a>
<a class="btn btn-default btn-xs visible-xs" v-if="shouldHaveQrcode(f.name)" href="{{genInstallURL(f.name)}}">
Install <i class="fa fa-cube"></i>
</a>
<a class="btn btn-default btn-xs" v-if="auth.delete" v-on:click="deletePathConfirm(f, $event)" href="javascript:void(0)">
<span style="color:#CC3300" class="glyphicon glyphicon-trash"></span>
</a>
</template>
</td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-12" id="preview" v-if="previewFile">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title" style="font-weight: normal">
<i class="fa" v-bind:class='genFileClass(previewFile)'></i>
{{previewFile.name}}
</h3>
</div>
<div class="panel-body">
<article class="markdown-body">{{{previewFile.contentHTML }}}
</article>
</div>
</div>
</div>
<div class="col-md-12" id="content">
<!-- Small qrcode modal -->
<div id="qrcode-modal" class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&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 (ver:{{version}})</a>, written by <a href="https://github.com/codeskyblue">codeskyblue</a>. 2016. go1.6
</div>
</div>
</div>
<script src="/-/res/js/jquery-3.1.0.min.js"></script>
<script src="/-/res/js/jquery.qrcode.js"></script>
<script src="/-/res/js/jquery.scrollUp.min.js"></script>
<script src="/-/res/js/qrcode.js"></script>
<script src="/-/res/js/vue-1.0.min.js"></script>
<script src="/-/res/js/showdown-1.4.2.min.js"></script>
<script src="/-/res/js/moment.min.js"></script>
<script src="/-/res/js/dropzone.js"></script>
<script src="/-/res/js/underscore-min.js"></script>
<script src="/-/res/bootstrap-3.3.5/js/bootstrap.min.js"></script>
<script src="/-/res/js/index.js"></script>
[[if .GoogleTrackerId ]]
<script>
(function(i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject'] = r;
i[r] = i[r] || function() {
(i[r].q = i[r].q || []).push(arguments)
}, i[r].l = 1 * new Date();
a = s.createElement(o),
m = s.getElementsByTagName(o)[0];
a.async = 1;
a.src = g;
m.parentNode.insertBefore(a, m)
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
ga('create', '[[.GoogleTrackerId]]', 'auto');
ga('send', 'pageview');
</script>
[[ end ]]
</body>
</html>

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,271 +0,0 @@
jQuery('#qrcodeCanvas').qrcode({
text: "http://jetienne.com/"
});
function getExtention(fname) {
return fname.slice((fname.lastIndexOf(".") - 1 >>> 0) + 2);
}
function pathJoin(parts, sep) {
var separator = sep || '/';
var replace = new RegExp(separator + '{1,}', 'g');
return parts.join(separator).replace(replace, separator);
}
function getQueryString(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
var r = decodeURI(window.location.search).substr(1).match(reg);
if (r != null) return r[2].replace(/\+/g, ' ');
return null;
}
var vm = new Vue({
el: "#app",
data: {
message: "Hello vue.js",
location: window.location,
breadcrumb: [],
showHidden: false,
previewFile: null,
version: "loading",
mtimeTypeFromNow: false, // or fromNow
auth: {},
search: getQueryString("search"),
files: [{
name: "loading ...",
path: "",
size: "...",
type: "dir",
}]
},
computed: {
computedFiles: function() {
var that = this;
this.previewFile = null;
var files = this.files.filter(function(f) {
if (f.name == 'README.md') {
that.previewFile = {
name: f.name,
path: f.path,
size: f.size,
type: 'markdown',
contentHTML: '',
}
}
if (!that.showHidden && f.name.slice(0, 1) === '.') {
return false;
}
return true;
});
// console.log(this.previewFile)
if (this.previewFile) {
var name = this.previewFile.name; // For now only README.md
console.log(pathJoin([location.pathname, 'README.md']))
$.ajax({
url: pathJoin([location.pathname, 'README.md']),
method: 'GET',
success: function(res) {
var converter = new showdown.Converter({
tables: true,
omitExtraWLInCodeBlocks: true,
parseImgDimensions: true,
simplifiedAutoLink: true,
literalMidWordUnderscores: true,
tasklists: true,
ghCodeBlocks: true,
smoothLivePreview: true,
});
var html = converter.makeHtml(res);
that.previewFile.contentHTML = html;
},
error: function(err) {
console.log(err)
}
})
}
return files;
},
},
methods: {
formatTime: function(timestamp) {
var m = moment(timestamp);
if (this.mtimeTypeFromNow) {
return m.fromNow();
}
return m.format('YYYY-MM-DD HH:mm:ss');
},
toggleHidden: function() {
this.showHidden = !this.showHidden;
},
genInstallURL: function(name) {
if (getExtention(name) == "ipa") {
urlPath = location.protocol + "//" + pathJoin([location.host, "/-/ipa/link", location.pathname, name]);
return urlPath;
}
return location.protocol + "//" + pathJoin([location.host, location.pathname, name]);
},
genQrcode: function(text, title) {
var urlPath = this.genInstallURL(text);
$("#qrcode-title").html(title || text);
$("#qrcode-link").attr("href", urlPath);
$('#qrcodeCanvas').empty().qrcode({
text: urlPath
});
$("#qrcode-modal").modal("show");
},
shouldHaveQrcode: function(name) {
return ['apk', 'ipa'].indexOf(getExtention(name)) !== -1;
},
genFileClass: function(f) {
if (f.type == "dir") {
if (f.name == '.git') {
return 'fa-git-square';
}
return "fa-folder-open";
}
var ext = getExtention(f.name);
switch (ext) {
case "go":
case "py":
case "js":
case "java":
case "c":
case "cpp":
case "h":
return "fa-file-code-o";
case "pdf":
return "fa-file-pdf-o";
case "zip":
return "fa-file-zip-o";
case "mp3":
case "wav":
return "fa-file-audio-o";
case "jpg":
case "png":
case "gif":
case "jpeg":
case "tiff":
return "fa-file-picture-o";
case "ipa":
case "dmg":
return "fa-apple";
case "apk":
return "fa-android";
case "exe":
return "fa-windows";
}
return "fa-file-text-o"
},
clickFileOrDir: function(f, e) {
if (f.type == "file") {
return true;
}
var reqPath = pathJoin([location.pathname, f.name]);
loadDirectory(reqPath);
e.preventDefault()
},
changePath: function(reqPath, e) {
loadDirectory(reqPath);
e.preventDefault()
},
deletePathConfirm: function(f, e) {
// confirm
e.preventDefault();
$.ajax({
url: pathJoin([location.pathname, f.name]),
method: 'DELETE',
success: function(res) {
loadFileList()
},
error: function(err) {
alert(err.responseText);
}
});
},
updateBreadcrumb: function() {
var pathname = decodeURI(location.pathname || "/");
var parts = pathname.split('/');
this.breadcrumb = [];
if (pathname == "/") {
return this.breadcrumb;
}
var i = 2;
for (; i <= parts.length; i += 1) {
var name = parts[i - 1];
var path = parts.slice(0, i).join('/');
this.breadcrumb.push({
name: name + (i == parts.length ? ' /' : ''),
path: path
})
}
return this.breadcrumb;
}
}
})
window.onpopstate = function(event) {
var pathname = decodeURI(location.pathname)
loadFileList()
}
function loadDirectory(reqPath) {
window.history.pushState({}, "", reqPath);
loadFileList(reqPath)
}
function loadFileList(pathname) {
var pathname = pathname || location.pathname;
// console.log("load filelist:", pathname)
$.ajax({
url: pathJoin(["/-/json", pathname]),
dataType: "json",
cache: false,
success: function(res) {
res.files = _.sortBy(res.files, function(f) {
return [f.type, f.name];
})
vm.files = res.files;
vm.auth = res.auth;
},
error: function(err) {
console.error(err)
},
});
vm.updateBreadcrumb();
}
// For page first loading
loadFileList(location.pathname + location.search)
// update version
$.getJSON("/-/sysinfo", function(res) {
vm.version = res.version;
})
Dropzone.options.myDropzone = {
paramName: "file",
maxFilesize: 1024,
addRemoveLinks: true,
init: function() {
this.on("uploadprogress", function(file, progress) {
console.log("File progress", progress);
});
this.on("complete", function(file) {
console.log("reload file list")
loadFileList()
})
}
}
$(function() {
$.scrollUp({
scrollText: '', // text are defined in css
});
});
Vue.filter('fromNow', function(value) {
return moment(value).fromNow();
})

File diff suppressed because one or more lines are too long

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

View file

@ -1,2 +1,7 @@
upload: true
delete: true
accessTables:
- regex: block.file
allow: false
- regex: visual.file
allow: true

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

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

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

View file

@ -1,2 +1,7 @@
---
upload: true
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")
@ -56,3 +55,28 @@ func SublimeContains(s, substr string) bool {
}
return ok
}
// getLocalIP returns the non loopback local IP of the host
func getLocalIP() string {
addrs, err := net.InterfaceAddrs()
if err != nil {
return ""
}
for _, address := range addrs {
// check the address type and if it is not a loopback the display it
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
return ipnet.IP.String()
}
}
}
return ""
}
func fileExists(path string) bool {
info, err := os.Stat(path)
if err != nil {
return false
}
return !info.IsDir()
}

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
}

View file

@ -1,565 +0,0 @@
package plist
import (
"bufio"
"encoding/hex"
"errors"
"io"
"runtime"
"strconv"
"strings"
"time"
)
type textPlistGenerator struct {
writer io.Writer
format int
quotableTable *[4]uint64
indent string
depth int
dictKvDelimiter, dictEntryDelimiter, arrayDelimiter []byte
}
var (
textPlistTimeLayout = "2006-01-02 15:04:05 -0700"
padding = "0000"
)
func (p *textPlistGenerator) generateDocument(pval *plistValue) {
p.writePlistValue(pval)
}
func (p *textPlistGenerator) plistQuotedString(str string) string {
if str == "" {
return `""`
}
s := ""
quot := false
for _, r := range str {
if r > 0xFF {
quot = true
s += `\U`
us := strconv.FormatInt(int64(r), 16)
s += padding[len(us):]
s += us
} else if r > 0x7F {
quot = true
s += `\`
us := strconv.FormatInt(int64(r), 8)
s += padding[1+len(us):]
s += us
} else {
c := uint8(r)
if (*p.quotableTable)[c/64]&(1<<(c%64)) > 0 {
quot = true
}
switch c {
case '\a':
s += `\a`
case '\b':
s += `\b`
case '\v':
s += `\v`
case '\f':
s += `\f`
case '\\':
s += `\\`
case '"':
s += `\"`
case '\t', '\r', '\n':
fallthrough
default:
s += string(c)
}
}
}
if quot {
s = `"` + s + `"`
}
return s
}
func (p *textPlistGenerator) deltaIndent(depthDelta int) {
if depthDelta < 0 {
p.depth--
} else if depthDelta > 0 {
p.depth++
}
}
func (p *textPlistGenerator) writeIndent() {
if len(p.indent) == 0 {
return
}
if len(p.indent) > 0 {
p.writer.Write([]byte("\n"))
for i := 0; i < p.depth; i++ {
io.WriteString(p.writer, p.indent)
}
}
}
func (p *textPlistGenerator) writePlistValue(pval *plistValue) {
if pval == nil {
return
}
switch pval.kind {
case Dictionary:
p.writer.Write([]byte(`{`))
p.deltaIndent(1)
dict := pval.value.(*dictionary)
dict.populateArrays()
for i, k := range dict.keys {
p.writeIndent()
io.WriteString(p.writer, p.plistQuotedString(k))
p.writer.Write(p.dictKvDelimiter)
p.writePlistValue(dict.values[i])
p.writer.Write(p.dictEntryDelimiter)
}
p.deltaIndent(-1)
p.writeIndent()
p.writer.Write([]byte(`}`))
case Array:
p.writer.Write([]byte(`(`))
p.deltaIndent(1)
values := pval.value.([]*plistValue)
for _, v := range values {
p.writeIndent()
p.writePlistValue(v)
p.writer.Write(p.arrayDelimiter)
}
p.deltaIndent(-1)
p.writeIndent()
p.writer.Write([]byte(`)`))
case String:
io.WriteString(p.writer, p.plistQuotedString(pval.value.(string)))
case Integer:
if p.format == GNUStepFormat {
p.writer.Write([]byte(`<*I`))
}
if pval.value.(signedInt).signed {
io.WriteString(p.writer, strconv.FormatInt(int64(pval.value.(signedInt).value), 10))
} else {
io.WriteString(p.writer, strconv.FormatUint(pval.value.(signedInt).value, 10))
}
if p.format == GNUStepFormat {
p.writer.Write([]byte(`>`))
}
case Real:
if p.format == GNUStepFormat {
p.writer.Write([]byte(`<*R`))
}
io.WriteString(p.writer, strconv.FormatFloat(pval.value.(sizedFloat).value, 'g', -1, 64))
if p.format == GNUStepFormat {
p.writer.Write([]byte(`>`))
}
case Boolean:
b := pval.value.(bool)
if p.format == GNUStepFormat {
if b {
p.writer.Write([]byte(`<*BY>`))
} else {
p.writer.Write([]byte(`<*BN>`))
}
} else {
if b {
p.writer.Write([]byte(`1`))
} else {
p.writer.Write([]byte(`0`))
}
}
case Data:
b := pval.value.([]byte)
var hexencoded [9]byte
var l int
var asc = 9
hexencoded[8] = ' '
p.writer.Write([]byte(`<`))
for i := 0; i < len(b); i += 4 {
l = i + 4
if l >= len(b) {
l = len(b)
// We no longer need the space - or the rest of the buffer.
// (we used >= above to get this part without another conditional :P)
asc = (l - i) * 2
}
// Fill the buffer (only up to 8 characters, to preserve the space we implicitly include
// at the end of every encode)
hex.Encode(hexencoded[:8], b[i:l])
io.WriteString(p.writer, string(hexencoded[:asc]))
}
p.writer.Write([]byte(`>`))
case Date:
if p.format == GNUStepFormat {
p.writer.Write([]byte(`<*D`))
io.WriteString(p.writer, pval.value.(time.Time).In(time.UTC).Format(textPlistTimeLayout))
p.writer.Write([]byte(`>`))
} else {
io.WriteString(p.writer, p.plistQuotedString(pval.value.(time.Time).In(time.UTC).Format(textPlistTimeLayout)))
}
}
}
func (p *textPlistGenerator) Indent(i string) {
p.indent = i
if i == "" {
p.dictKvDelimiter = []byte(`=`)
} else {
// For pretty-printing
p.dictKvDelimiter = []byte(` = `)
}
}
func newTextPlistGenerator(w io.Writer, format int) *textPlistGenerator {
table := &osQuotable
if format == GNUStepFormat {
table = &gsQuotable
}
return &textPlistGenerator{
writer: mustWriter{w},
format: format,
quotableTable: table,
dictKvDelimiter: []byte(`=`),
arrayDelimiter: []byte(`,`),
dictEntryDelimiter: []byte(`;`),
}
}
type byteReader interface {
io.Reader
io.ByteScanner
Peek(n int) ([]byte, error)
ReadBytes(delim byte) ([]byte, error)
}
type textPlistParser struct {
reader byteReader
whitespaceReplacer *strings.Replacer
format int
}
func (p *textPlistParser) parseDocument() (pval *plistValue, parseError error) {
defer func() {
if r := recover(); r != nil {
if _, ok := r.(runtime.Error); ok {
panic(r)
}
if _, ok := r.(invalidPlistError); ok {
parseError = r.(error)
} else {
// Wrap all non-invalid-plist errors.
parseError = plistParseError{"text", r.(error)}
}
}
}()
pval = p.parsePlistValue()
return
}
func (p *textPlistParser) chugWhitespace() {
ws:
for {
c, err := p.reader.ReadByte()
if err != nil && err != io.EOF {
panic(err)
}
if whitespace[c/64]&(1<<(c%64)) == 0 {
if c == '/' && err != io.EOF {
// A / at the end of the file is not the begining of a comment.
cs, err := p.reader.Peek(1)
if err != nil && err != io.EOF {
panic(err)
}
c = cs[0]
switch c {
case '/':
for {
c, err = p.reader.ReadByte()
if err != nil && err != io.EOF {
panic(err)
} else if err == io.EOF {
break
}
// TODO: UTF-8
if c == '\n' || c == '\r' {
break
}
}
case '*':
// Peek returned a value here, so it is safe to read.
_, _ = p.reader.ReadByte()
star := false
for {
c, err = p.reader.ReadByte()
if err != nil {
panic(err)
}
if c == '*' {
star = true
} else if c == '/' && star {
break
} else {
star = false
}
}
default:
p.reader.UnreadByte() // Not the beginning of a // or /* comment
break ws
}
continue
}
p.reader.UnreadByte()
break
}
}
}
func (p *textPlistParser) parseQuotedString() *plistValue {
escaping := false
s := ""
for {
byt, err := p.reader.ReadByte()
// EOF here is an error: we're inside a quoted string!
if err != nil {
panic(err)
}
c := rune(byt)
if !escaping {
if c == '"' {
break
} else if c == '\\' {
escaping = true
continue
}
} else {
escaping = false
// Everything that is not listed here passes through unharmed.
switch c {
case 'a':
c = '\a'
case 'b':
c = '\b'
case 'v':
c = '\v'
case 'f':
c = '\f'
case 't':
c = '\t'
case 'r':
c = '\r'
case 'n':
c = '\n'
case 'x', 'u', 'U': // hex and unicode
l := 4
if c == 'x' {
l = 2
}
hex := make([]byte, l)
p.reader.Read(hex)
newc := mustParseInt(string(hex), 16, 16)
c = rune(newc)
case '0', '1', '2', '3', '4', '5', '6', '7': // octal!
oct := make([]byte, 3)
oct[0] = uint8(c)
p.reader.Read(oct[1:])
newc := mustParseInt(string(oct), 8, 16)
c = rune(newc)
}
}
s += string(c)
}
return &plistValue{String, s}
}
func (p *textPlistParser) parseUnquotedString() *plistValue {
s := ""
for {
c, err := p.reader.ReadByte()
if err != nil {
if err == io.EOF {
break
}
panic(err)
}
// if we encounter a character that must be quoted, we're done.
// the GNUStep quote table is more lax here, so we use it instead of the OpenStep one.
if gsQuotable[c/64]&(1<<(c%64)) > 0 {
p.reader.UnreadByte()
break
}
s += string(c)
}
return &plistValue{String, s}
}
func (p *textPlistParser) parseDictionary() *plistValue {
var keypv *plistValue
subval := make(map[string]*plistValue)
for {
p.chugWhitespace()
c, err := p.reader.ReadByte()
// EOF here is an error: we're inside a dictionary!
if err != nil {
panic(err)
}
if c == '}' {
break
} else if c == '"' {
keypv = p.parseQuotedString()
} else {
p.reader.UnreadByte() // Whoops, ate part of the string
keypv = p.parseUnquotedString()
}
if keypv == nil {
// TODO better error
panic(errors.New("missing dictionary key"))
}
p.chugWhitespace()
c, err = p.reader.ReadByte()
if err != nil {
panic(err)
}
if c != '=' {
panic(errors.New("missing = in dictionary"))
}
// whitespace is guzzled within
val := p.parsePlistValue()
p.chugWhitespace()
c, err = p.reader.ReadByte()
if err != nil {
panic(err)
}
if c != ';' {
panic(errors.New("missing ; in dictionary"))
}
subval[keypv.value.(string)] = val
}
return &plistValue{Dictionary, &dictionary{m: subval}}
}
func (p *textPlistParser) parseArray() *plistValue {
subval := make([]*plistValue, 0, 10)
for {
c, err := p.reader.ReadByte()
// EOF here is an error: we're inside an array!
if err != nil {
panic(err)
}
if c == ')' {
break
} else if c == ',' {
continue
}
p.reader.UnreadByte()
pval := p.parsePlistValue()
if pval.kind == String && pval.value.(string) == "" {
continue
}
subval = append(subval, pval)
}
return &plistValue{Array, subval}
}
func (p *textPlistParser) parseGNUStepValue(v []byte) *plistValue {
if len(v) < 2 {
panic(errors.New("invalid GNUStep extended value"))
}
typ := v[1]
v = v[2:]
switch typ {
case 'I':
if v[0] == '-' {
n := mustParseInt(string(v), 10, 64)
return &plistValue{Integer, signedInt{uint64(n), true}}
} else {
n := mustParseUint(string(v), 10, 64)
return &plistValue{Integer, signedInt{n, false}}
}
case 'R':
n := mustParseFloat(string(v), 64)
return &plistValue{Real, sizedFloat{n, 64}}
case 'B':
b := v[0] == 'Y'
return &plistValue{Boolean, b}
case 'D':
t, err := time.Parse(textPlistTimeLayout, string(v))
if err != nil {
panic(err)
}
return &plistValue{Date, t.In(time.UTC)}
}
panic(errors.New("invalid GNUStep type " + string(typ)))
return nil
}
func (p *textPlistParser) parsePlistValue() *plistValue {
for {
p.chugWhitespace()
c, err := p.reader.ReadByte()
if err != nil && err != io.EOF {
panic(err)
}
switch c {
case '<':
bytes, err := p.reader.ReadBytes('>')
if err != nil {
panic(err)
}
bytes = bytes[:len(bytes)-1]
if bytes[0] == '*' {
p.format = GNUStepFormat
return p.parseGNUStepValue(bytes)
} else {
s := p.whitespaceReplacer.Replace(string(bytes))
data, err := hex.DecodeString(s)
if err != nil {
panic(err)
}
return &plistValue{Data, data}
}
case '"':
return p.parseQuotedString()
case '{':
return p.parseDictionary()
case '(':
return p.parseArray()
default:
p.reader.UnreadByte() // Place back in buffer for parseUnquotedString
return p.parseUnquotedString()
}
}
return nil
}
func newTextPlistParser(r io.Reader) *textPlistParser {
var reader byteReader
if rd, ok := r.(byteReader); ok {
reader = rd
} else {
reader = bufio.NewReader(r)
}
return &textPlistParser{
reader: reader,
whitespaceReplacer: strings.NewReplacer("\t", "", "\n", "", " ", "", "\r", ""),
format: OpenStepFormat,
}
}

View file

@ -1,26 +0,0 @@
package plist
// Bitmap of characters that must be inside a quoted string
// when written to an old-style property list
// Low bits represent lower characters, and each uint64 represents 64 characters.
var gsQuotable = [4]uint64{
0x78001385ffffffff,
0xa800000138000000,
0xffffffffffffffff,
0xffffffffffffffff,
}
// 7f instead of 3f in the top line: CFOldStylePlist.c says . is valid, but they quote it.
var osQuotable = [4]uint64{
0xf4007f6fffffffff,
0xf8000001f8000001,
0xffffffffffffffff,
0xffffffffffffffff,
}
var whitespace = [4]uint64{
0x0000000100003f00,
0x0000000000000000,
0x0000000000000000,
0x0000000000000000,
}

View file

@ -1,170 +0,0 @@
package plist
import (
"reflect"
"strings"
"sync"
)
// typeInfo holds details for the plist representation of a type.
type typeInfo struct {
fields []fieldInfo
}
// fieldInfo holds details for the plist representation of a single field.
type fieldInfo struct {
idx []int
name string
omitEmpty bool
}
var tinfoMap = make(map[reflect.Type]*typeInfo)
var tinfoLock sync.RWMutex
// getTypeInfo returns the typeInfo structure with details necessary
// for marshalling and unmarshalling typ.
func getTypeInfo(typ reflect.Type) (*typeInfo, error) {
tinfoLock.RLock()
tinfo, ok := tinfoMap[typ]
tinfoLock.RUnlock()
if ok {
return tinfo, nil
}
tinfo = &typeInfo{}
if typ.Kind() == reflect.Struct {
n := typ.NumField()
for i := 0; i < n; i++ {
f := typ.Field(i)
if f.PkgPath != "" || f.Tag.Get("plist") == "-" {
continue // Private field
}
// For embedded structs, embed its fields.
if f.Anonymous {
t := f.Type
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() == reflect.Struct {
inner, err := getTypeInfo(t)
if err != nil {
return nil, err
}
for _, finfo := range inner.fields {
finfo.idx = append([]int{i}, finfo.idx...)
if err := addFieldInfo(typ, tinfo, &finfo); err != nil {
return nil, err
}
}
continue
}
}
finfo, err := structFieldInfo(typ, &f)
if err != nil {
return nil, err
}
// Add the field if it doesn't conflict with other fields.
if err := addFieldInfo(typ, tinfo, finfo); err != nil {
return nil, err
}
}
}
tinfoLock.Lock()
tinfoMap[typ] = tinfo
tinfoLock.Unlock()
return tinfo, nil
}
// structFieldInfo builds and returns a fieldInfo for f.
func structFieldInfo(typ reflect.Type, f *reflect.StructField) (*fieldInfo, error) {
finfo := &fieldInfo{idx: f.Index}
// Split the tag from the xml namespace if necessary.
tag := f.Tag.Get("plist")
// Parse flags.
tokens := strings.Split(tag, ",")
tag = tokens[0]
if len(tokens) > 1 {
tag = tokens[0]
for _, flag := range tokens[1:] {
switch flag {
case "omitempty":
finfo.omitEmpty = true
}
}
}
if tag == "" {
// If the name part of the tag is completely empty,
// use the field name
finfo.name = f.Name
return finfo, nil
}
finfo.name = tag
return finfo, nil
}
// addFieldInfo adds finfo to tinfo.fields if there are no
// conflicts, or if conflicts arise from previous fields that were
// obtained from deeper embedded structures than finfo. In the latter
// case, the conflicting entries are dropped.
// A conflict occurs when the path (parent + name) to a field is
// itself a prefix of another path, or when two paths match exactly.
// It is okay for field paths to share a common, shorter prefix.
func addFieldInfo(typ reflect.Type, tinfo *typeInfo, newf *fieldInfo) error {
var conflicts []int
// First, figure all conflicts. Most working code will have none.
for i := range tinfo.fields {
oldf := &tinfo.fields[i]
if newf.name == oldf.name {
conflicts = append(conflicts, i)
}
}
// Without conflicts, add the new field and return.
if conflicts == nil {
tinfo.fields = append(tinfo.fields, *newf)
return nil
}
// If any conflict is shallower, ignore the new field.
// This matches the Go field resolution on embedding.
for _, i := range conflicts {
if len(tinfo.fields[i].idx) < len(newf.idx) {
return nil
}
}
// Otherwise, the new field is shallower, and thus takes precedence,
// so drop the conflicting fields from tinfo and append the new one.
for c := len(conflicts) - 1; c >= 0; c-- {
i := conflicts[c]
copy(tinfo.fields[i:], tinfo.fields[i+1:])
tinfo.fields = tinfo.fields[:len(tinfo.fields)-1]
}
tinfo.fields = append(tinfo.fields, *newf)
return nil
}
// value returns v's field value corresponding to finfo.
// It's equivalent to v.FieldByIndex(finfo.idx), but initializes
// and dereferences pointers as necessary.
func (finfo *fieldInfo) value(v reflect.Value) reflect.Value {
for i, x := range finfo.idx {
if i > 0 {
t := v.Type()
if t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct {
if v.IsNil() {
v.Set(reflect.New(v.Type().Elem()))
}
v = v.Elem()
}
}
v = v.Field(x)
}
return v
}

View file

@ -1,276 +0,0 @@
package plist
import (
"encoding"
"fmt"
"reflect"
"time"
)
type incompatibleDecodeTypeError struct {
typ reflect.Type
pKind plistKind
}
func (u *incompatibleDecodeTypeError) Error() string {
return fmt.Sprintf("plist: type mismatch: tried to decode %v into value of type %v", plistKindNames[u.pKind], u.typ)
}
var (
textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
)
func isEmptyInterface(v reflect.Value) bool {
return v.Kind() == reflect.Interface && v.NumMethod() == 0
}
func (p *Decoder) unmarshalTextInterface(pval *plistValue, unmarshalable encoding.TextUnmarshaler) {
err := unmarshalable.UnmarshalText([]byte(pval.value.(string)))
if err != nil {
panic(err)
}
}
func (p *Decoder) unmarshalTime(pval *plistValue, val reflect.Value) {
val.Set(reflect.ValueOf(pval.value.(time.Time)))
}
func (p *Decoder) unmarshalLaxString(s string, val reflect.Value) {
switch val.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
i := mustParseInt(s, 10, 64)
val.SetInt(i)
return
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
i := mustParseUint(s, 10, 64)
val.SetUint(i)
return
case reflect.Float32, reflect.Float64:
f := mustParseFloat(s, 64)
val.SetFloat(f)
return
case reflect.Bool:
b := mustParseBool(s)
val.SetBool(b)
return
case reflect.Struct:
if val.Type() == timeType {
t, err := time.Parse(textPlistTimeLayout, s)
if err != nil {
panic(err)
}
val.Set(reflect.ValueOf(t.In(time.UTC)))
return
}
fallthrough
default:
panic(&incompatibleDecodeTypeError{val.Type(), String})
}
}
func (p *Decoder) unmarshal(pval *plistValue, val reflect.Value) {
if pval == nil {
return
}
if val.Kind() == reflect.Ptr {
if val.IsNil() {
val.Set(reflect.New(val.Type().Elem()))
}
val = val.Elem()
}
if isEmptyInterface(val) {
v := p.valueInterface(pval)
val.Set(reflect.ValueOf(v))
return
}
incompatibleTypeError := &incompatibleDecodeTypeError{val.Type(), pval.kind}
// time.Time implements TextMarshaler, but we need to parse it as RFC3339
if pval.kind == Date {
if val.Type() == timeType {
p.unmarshalTime(pval, val)
return
}
panic(incompatibleTypeError)
}
if val.CanInterface() && val.Type().Implements(textUnmarshalerType) && val.Type() != timeType {
p.unmarshalTextInterface(pval, val.Interface().(encoding.TextUnmarshaler))
return
}
if val.CanAddr() {
pv := val.Addr()
if pv.CanInterface() && pv.Type().Implements(textUnmarshalerType) && val.Type() != timeType {
p.unmarshalTextInterface(pval, pv.Interface().(encoding.TextUnmarshaler))
return
}
}
typ := val.Type()
switch pval.kind {
case String:
if val.Kind() == reflect.String {
val.SetString(pval.value.(string))
return
}
if p.lax {
p.unmarshalLaxString(pval.value.(string), val)
return
}
panic(incompatibleTypeError)
case Integer:
switch val.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
val.SetInt(int64(pval.value.(signedInt).value))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
val.SetUint(pval.value.(signedInt).value)
default:
panic(incompatibleTypeError)
}
case Real:
if val.Kind() == reflect.Float32 || val.Kind() == reflect.Float64 {
val.SetFloat(pval.value.(sizedFloat).value)
} else {
panic(incompatibleTypeError)
}
case Boolean:
if val.Kind() == reflect.Bool {
val.SetBool(pval.value.(bool))
} else {
panic(incompatibleTypeError)
}
case Data:
if val.Kind() == reflect.Slice && typ.Elem().Kind() == reflect.Uint8 {
val.SetBytes(pval.value.([]byte))
} else {
panic(incompatibleTypeError)
}
case Array:
p.unmarshalArray(pval, val)
case Dictionary:
p.unmarshalDictionary(pval, val)
}
}
func (p *Decoder) unmarshalArray(pval *plistValue, val reflect.Value) {
subvalues := pval.value.([]*plistValue)
var n int
if val.Kind() == reflect.Slice {
// Slice of element values.
// Grow slice.
cnt := len(subvalues) + val.Len()
if cnt >= val.Cap() {
ncap := 2 * cnt
if ncap < 4 {
ncap = 4
}
new := reflect.MakeSlice(val.Type(), val.Len(), ncap)
reflect.Copy(new, val)
val.Set(new)
}
n = val.Len()
val.SetLen(cnt)
} else if val.Kind() == reflect.Array {
if len(subvalues) > val.Cap() {
panic(fmt.Errorf("plist: attempted to unmarshal %d values into an array of size %d", len(subvalues), val.Cap()))
}
} else {
panic(&incompatibleDecodeTypeError{val.Type(), pval.kind})
}
// Recur to read element into slice.
for _, sval := range subvalues {
p.unmarshal(sval, val.Index(n))
n++
}
return
}
func (p *Decoder) unmarshalDictionary(pval *plistValue, val reflect.Value) {
typ := val.Type()
switch val.Kind() {
case reflect.Struct:
tinfo, err := getTypeInfo(typ)
if err != nil {
panic(err)
}
subvalues := pval.value.(*dictionary).m
for _, finfo := range tinfo.fields {
p.unmarshal(subvalues[finfo.name], finfo.value(val))
}
case reflect.Map:
if val.IsNil() {
val.Set(reflect.MakeMap(typ))
}
subvalues := pval.value.(*dictionary).m
for k, sval := range subvalues {
keyv := reflect.ValueOf(k).Convert(typ.Key())
mapElem := val.MapIndex(keyv)
if !mapElem.IsValid() {
mapElem = reflect.New(typ.Elem()).Elem()
}
p.unmarshal(sval, mapElem)
val.SetMapIndex(keyv, mapElem)
}
default:
panic(&incompatibleDecodeTypeError{typ, pval.kind})
}
}
/* *Interface is modelled after encoding/json */
func (p *Decoder) valueInterface(pval *plistValue) interface{} {
switch pval.kind {
case String:
return pval.value.(string)
case Integer:
if pval.value.(signedInt).signed {
return int64(pval.value.(signedInt).value)
}
return pval.value.(signedInt).value
case Real:
bits := pval.value.(sizedFloat).bits
switch bits {
case 32:
return float32(pval.value.(sizedFloat).value)
case 64:
return pval.value.(sizedFloat).value
}
case Boolean:
return pval.value.(bool)
case Array:
return p.arrayInterface(pval.value.([]*plistValue))
case Dictionary:
return p.dictionaryInterface(pval.value.(*dictionary))
case Data:
return pval.value.([]byte)
case Date:
return pval.value.(time.Time)
}
return nil
}
func (p *Decoder) arrayInterface(subvalues []*plistValue) []interface{} {
out := make([]interface{}, len(subvalues))
for i, subv := range subvalues {
out[i] = p.valueInterface(subv)
}
return out
}
func (p *Decoder) dictionaryInterface(dict *dictionary) map[string]interface{} {
out := make(map[string]interface{})
for k, subv := range dict.m {
out[k] = p.valueInterface(subv)
}
return out
}

View file

@ -1,18 +0,0 @@
package plist
import "io"
type countedWriter struct {
io.Writer
nbytes int
}
func (w *countedWriter) Write(p []byte) (int, error) {
n, err := w.Writer.Write(p)
w.nbytes += n
return n, err
}
func (w *countedWriter) BytesWritten() int {
return w.nbytes
}

View file

@ -1,314 +0,0 @@
package plist
import (
"encoding/base64"
"encoding/xml"
"errors"
"fmt"
"io"
"math"
"runtime"
"strings"
"time"
)
const xmlDOCTYPE = `<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
`
type xmlPlistGenerator struct {
writer io.Writer
xmlEncoder *xml.Encoder
}
func (p *xmlPlistGenerator) generateDocument(pval *plistValue) {
io.WriteString(p.writer, xml.Header)
io.WriteString(p.writer, xmlDOCTYPE)
plistStartElement := xml.StartElement{
Name: xml.Name{
Space: "",
Local: "plist",
},
Attr: []xml.Attr{{
Name: xml.Name{
Space: "",
Local: "version"},
Value: "1.0"},
},
}
p.xmlEncoder.EncodeToken(plistStartElement)
p.writePlistValue(pval)
p.xmlEncoder.EncodeToken(plistStartElement.End())
p.xmlEncoder.Flush()
}
func (p *xmlPlistGenerator) writePlistValue(pval *plistValue) {
if pval == nil {
return
}
defer p.xmlEncoder.Flush()
key := ""
encodedValue := pval.value
switch pval.kind {
case Dictionary:
startElement := xml.StartElement{Name: xml.Name{Local: "dict"}}
p.xmlEncoder.EncodeToken(startElement)
dict := encodedValue.(*dictionary)
dict.populateArrays()
for i, k := range dict.keys {
p.xmlEncoder.EncodeElement(k, xml.StartElement{Name: xml.Name{Local: "key"}})
p.writePlistValue(dict.values[i])
}
p.xmlEncoder.EncodeToken(startElement.End())
case Array:
startElement := xml.StartElement{Name: xml.Name{Local: "array"}}
p.xmlEncoder.EncodeToken(startElement)
values := encodedValue.([]*plistValue)
for _, v := range values {
p.writePlistValue(v)
}
p.xmlEncoder.EncodeToken(startElement.End())
case String:
key = "string"
case Integer:
key = "integer"
if pval.value.(signedInt).signed {
encodedValue = int64(pval.value.(signedInt).value)
} else {
encodedValue = pval.value.(signedInt).value
}
case Real:
key = "real"
encodedValue = pval.value.(sizedFloat).value
switch {
case math.IsInf(pval.value.(sizedFloat).value, 1):
encodedValue = "inf"
case math.IsInf(pval.value.(sizedFloat).value, -1):
encodedValue = "-inf"
case math.IsNaN(pval.value.(sizedFloat).value):
encodedValue = "nan"
}
case Boolean:
key = "false"
b := pval.value.(bool)
if b {
key = "true"
}
encodedValue = ""
case Data:
key = "data"
encodedValue = xml.CharData(base64.StdEncoding.EncodeToString(pval.value.([]byte)))
case Date:
key = "date"
encodedValue = pval.value.(time.Time).In(time.UTC).Format(time.RFC3339)
}
if key != "" {
err := p.xmlEncoder.EncodeElement(encodedValue, xml.StartElement{Name: xml.Name{Local: key}})
if err != nil {
panic(err)
}
}
}
func (p *xmlPlistGenerator) Indent(i string) {
p.xmlEncoder.Indent("", i)
}
func newXMLPlistGenerator(w io.Writer) *xmlPlistGenerator {
mw := mustWriter{w}
return &xmlPlistGenerator{mw, xml.NewEncoder(mw)}
}
type xmlPlistParser struct {
reader io.Reader
xmlDecoder *xml.Decoder
whitespaceReplacer *strings.Replacer
ntags int
}
func (p *xmlPlistParser) parseDocument() (pval *plistValue, parseError error) {
defer func() {
if r := recover(); r != nil {
if _, ok := r.(runtime.Error); ok {
panic(r)
}
if _, ok := r.(invalidPlistError); ok {
parseError = r.(error)
} else {
// Wrap all non-invalid-plist errors.
parseError = plistParseError{"XML", r.(error)}
}
}
}()
for {
if token, err := p.xmlDecoder.Token(); err == nil {
if element, ok := token.(xml.StartElement); ok {
pval = p.parseXMLElement(element)
if p.ntags == 0 {
panic(invalidPlistError{"XML", errors.New("no elements encountered")})
}
return
}
} else {
// The first XML parse turned out to be invalid:
// we do not have an XML property list.
panic(invalidPlistError{"XML", err})
}
}
}
func (p *xmlPlistParser) parseXMLElement(element xml.StartElement) *plistValue {
var charData xml.CharData
switch element.Name.Local {
case "plist":
p.ntags++
for {
token, err := p.xmlDecoder.Token()
if err != nil {
panic(err)
}
if el, ok := token.(xml.EndElement); ok && el.Name.Local == "plist" {
break
}
if el, ok := token.(xml.StartElement); ok {
return p.parseXMLElement(el)
}
}
return nil
case "string":
p.ntags++
err := p.xmlDecoder.DecodeElement(&charData, &element)
if err != nil {
panic(err)
}
return &plistValue{String, string(charData)}
case "integer":
p.ntags++
err := p.xmlDecoder.DecodeElement(&charData, &element)
if err != nil {
panic(err)
}
s := string(charData)
if s[0] == '-' {
n := mustParseInt(string(charData), 10, 64)
return &plistValue{Integer, signedInt{uint64(n), true}}
} else {
n := mustParseUint(string(charData), 10, 64)
return &plistValue{Integer, signedInt{n, false}}
}
case "real":
p.ntags++
err := p.xmlDecoder.DecodeElement(&charData, &element)
if err != nil {
panic(err)
}
n := mustParseFloat(string(charData), 64)
return &plistValue{Real, sizedFloat{n, 64}}
case "true", "false":
p.ntags++
p.xmlDecoder.Skip()
b := element.Name.Local == "true"
return &plistValue{Boolean, b}
case "date":
p.ntags++
err := p.xmlDecoder.DecodeElement(&charData, &element)
if err != nil {
panic(err)
}
t, err := time.ParseInLocation(time.RFC3339, string(charData), time.UTC)
if err != nil {
panic(err)
}
return &plistValue{Date, t}
case "data":
p.ntags++
err := p.xmlDecoder.DecodeElement(&charData, &element)
if err != nil {
panic(err)
}
str := p.whitespaceReplacer.Replace(string(charData))
l := base64.StdEncoding.DecodedLen(len(str))
bytes := make([]uint8, l)
l, err = base64.StdEncoding.Decode(bytes, []byte(str))
if err != nil {
panic(err)
}
return &plistValue{Data, bytes[:l]}
case "dict":
p.ntags++
var key *string
var subvalues map[string]*plistValue = make(map[string]*plistValue)
for {
token, err := p.xmlDecoder.Token()
if err != nil {
panic(err)
}
if el, ok := token.(xml.EndElement); ok && el.Name.Local == "dict" {
if key != nil {
panic(errors.New("missing value in dictionary"))
}
break
}
if el, ok := token.(xml.StartElement); ok {
if el.Name.Local == "key" {
var k string
p.xmlDecoder.DecodeElement(&k, &el)
key = &k
} else {
if key == nil {
panic(errors.New("missing key in dictionary"))
}
subvalues[*key] = p.parseXMLElement(el)
key = nil
}
}
}
return &plistValue{Dictionary, &dictionary{m: subvalues}}
case "array":
p.ntags++
var subvalues []*plistValue = make([]*plistValue, 0, 10)
for {
token, err := p.xmlDecoder.Token()
if err != nil {
panic(err)
}
if el, ok := token.(xml.EndElement); ok && el.Name.Local == "array" {
break
}
if el, ok := token.(xml.StartElement); ok {
subvalues = append(subvalues, p.parseXMLElement(el))
}
}
return &plistValue{Array, subvalues}
}
err := fmt.Errorf("encountered unknown element %s", element.Name.Local)
if p.ntags == 0 {
// If out first XML tag is invalid, it might be an openstep data element, ala <abab> or <0101>
panic(invalidPlistError{"XML", err})
}
panic(err)
}
func newXMLPlistParser(r io.Reader) *xmlPlistParser {
return &xmlPlistParser{r, xml.NewDecoder(r), strings.NewReplacer("\t", "", "\n", "", " ", "", "\r", ""), 0}
}

View file

@ -1,4 +0,0 @@
sudo: false
language: go
install: go get -t -v ./...
go: 1.2

View file

@ -1,19 +0,0 @@
Copyright (C) 2014 Alec Thomas
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,560 +0,0 @@
# Kingpin - A Go (golang) command line and flag parser [![Build Status](https://travis-ci.org/alecthomas/kingpin.png)](https://travis-ci.org/alecthomas/kingpin)
<!-- MarkdownTOC -->
- [Overview](#overview)
- [Features](#features)
- [User-visible changes between v1 and v2](#user-visible-changes-between-v1-and-v2)
- [Flags can be used at any point after their definition.](#flags-can-be-used-at-any-point-after-their-definition)
- [Short flags can be combined with their parameters](#short-flags-can-be-combined-with-their-parameters)
- [API changes between v1 and v2](#api-changes-between-v1-and-v2)
- [Versions](#versions)
- [V2 is the current stable version](#v2-is-the-current-stable-version)
- [V1 is the OLD stable version](#v1-is-the-old-stable-version)
- [Change History](#change-history)
- [Examples](#examples)
- [Simple Example](#simple-example)
- [Complex Example](#complex-example)
- [Reference Documentation](#reference-documentation)
- [Displaying errors and usage information](#displaying-errors-and-usage-information)
- [Sub-commands](#sub-commands)
- [Custom Parsers](#custom-parsers)
- [Default Values](#default-values)
- [Place-holders in Help](#place-holders-in-help)
- [Consuming all remaining arguments](#consuming-all-remaining-arguments)
- [Supporting -h for help](#supporting--h-for-help)
- [Custom help](#custom-help)
<!-- /MarkdownTOC -->
## Overview
Kingpin is a [fluent-style](http://en.wikipedia.org/wiki/Fluent_interface),
type-safe command-line parser. It supports flags, nested commands, and
positional arguments.
Install it with:
$ go get gopkg.in/alecthomas/kingpin.v2
It looks like this:
```go
var (
verbose = kingpin.Flag("verbose", "Verbose mode.").Short('v').Bool()
name = kingpin.Arg("name", "Name of user.").Required().String()
)
func main() {
kingpin.Parse()
fmt.Printf("%v, %s\n", *verbose, *name)
}
```
More [examples](https://github.com/alecthomas/kingpin/tree/master/examples) are available.
Second to parsing, providing the user with useful help is probably the most
important thing a command-line parser does. Kingpin tries to provide detailed
contextual help if `--help` is encountered at any point in the command line
(excluding after `--`).
## Features
- Help output that isn't as ugly as sin.
- Fully [customisable help](#custom-help), via Go templates.
- Parsed, type-safe flags (`kingpin.Flag("f", "help").Int()`)
- Parsed, type-safe positional arguments (`kingpin.Arg("a", "help").Int()`).
- Parsed, type-safe, arbitrarily deep commands (`kingpin.Command("c", "help")`).
- Support for required flags and required positional arguments (`kingpin.Flag("f", "").Required().Int()`).
- Support for arbitrarily nested default commands (`command.Default()`).
- Callbacks per command, flag and argument (`kingpin.Command("c", "").Action(myAction)`).
- POSIX-style short flag combining (`-a -b` -> `-ab`).
- Short-flag+parameter combining (`-a parm` -> `-aparm`).
- Read command-line from files (`@<file>`).
- Automatically generate man pages (`--man-page`).
## User-visible changes between v1 and v2
### Flags can be used at any point after their definition.
Flags can be specified at any point after their definition, not just
*immediately after their associated command*. From the chat example below, the
following used to be required:
```
$ chat --server=chat.server.com:8080 post --image=~/Downloads/owls.jpg pics
```
But the following will now work:
```
$ chat post --server=chat.server.com:8080 --image=~/Downloads/owls.jpg pics
```
### Short flags can be combined with their parameters
Previously, if a short flag was used, any argument to that flag would have to
be separated by a space. That is no longer the case.
## API changes between v1 and v2
- `ParseWithFileExpansion()` is gone. The new parser directly supports expanding `@<file>`.
- Added `FatalUsage()` and `FatalUsageContext()` for displaying an error + usage and terminating.
- `Dispatch()` renamed to `Action()`.
- Added `ParseContext()` for parsing a command line into its intermediate context form without executing.
- Added `Terminate()` function to override the termination function.
- Added `UsageForContextWithTemplate()` for printing usage via a custom template.
- Added `UsageTemplate()` for overriding the default template to use. Two templates are included:
1. `DefaultUsageTemplate` - default template.
2. `CompactUsageTemplate` - compact command template for larger applications.
## Versions
Kingpin uses [gopkg.in](https://gopkg.in/alecthomas/kingpin) for versioning.
The current stable version is [gopkg.in/alecthomas/kingpin.v2](https://gopkg.in/alecthomas/kingpin.v2). The previous version, [gopkg.in/alecthomas/kingpin.v1](https://gopkg.in/alecthomas/kingpin.v1), is deprecated and in maintenance mode.
### [V2](https://gopkg.in/alecthomas/kingpin.v2) is the current stable version
Installation:
```sh
$ go get gopkg.in/alecthomas/kingpin.v2
```
### [V1](https://gopkg.in/alecthomas/kingpin.v1) is the OLD stable version
Installation:
```sh
$ go get gopkg.in/alecthomas/kingpin.v1
```
## Change History
- *2015-09-19* -- Stable v2.1.0 release.
- Added `command.Default()` to specify a default command to use if no other
command matches. This allows for convenient user shortcuts.
- Exposed `HelpFlag` and `VersionFlag` for further cusomisation.
- `Action()` and `PreAction()` added and both now support an arbitrary
number of callbacks.
- `kingpin.SeparateOptionalFlagsUsageTemplate`.
- `--help-long` and `--help-man` (hidden by default) flags.
- Flags are "interspersed" by default, but can be disabled with `app.Interspersed(false)`.
- Added flags for all simple builtin types (int8, uint16, etc.) and slice variants.
- Use `app.Writer(os.Writer)` to specify the default writer for all output functions.
- Dropped `os.Writer` prefix from all printf-like functions.
- *2015-05-22* -- Stable v2.0.0 release.
- Initial stable release of v2.0.0.
- Fully supports interspersed flags, commands and arguments.
- Flags can be present at any point after their logical definition.
- Application.Parse() terminates if commands are present and a command is not parsed.
- Dispatch() -> Action().
- Actions are dispatched after all values are populated.
- Override termination function (defaults to os.Exit).
- Override output stream (defaults to os.Stderr).
- Templatised usage help, with default and compact templates.
- Make error/usage functions more consistent.
- Support argument expansion from files by default (with @<file>).
- Fully public data model is available via .Model().
- Parser has been completely refactored.
- Parsing and execution has been split into distinct stages.
- Use `go generate` to generate repeated flags.
- Support combined short-flag+argument: -fARG.
- *2015-01-23* -- Stable v1.3.4 release.
- Support "--" for separating flags from positional arguments.
- Support loading flags from files (ParseWithFileExpansion()). Use @FILE as an argument.
- Add post-app and post-cmd validation hooks. This allows arbitrary validation to be added.
- A bunch of improvements to help usage and formatting.
- Support arbitrarily nested sub-commands.
- *2014-07-08* -- Stable v1.2.0 release.
- Pass any value through to `Strings()` when final argument.
Allows for values that look like flags to be processed.
- Allow `--help` to be used with commands.
- Support `Hidden()` flags.
- Parser for [units.Base2Bytes](https://github.com/alecthomas/units)
type. Allows for flags like `--ram=512MB` or `--ram=1GB`.
- Add an `Enum()` value, allowing only one of a set of values
to be selected. eg. `Flag(...).Enum("debug", "info", "warning")`.
- *2014-06-27* -- Stable v1.1.0 release.
- Bug fixes.
- Always return an error (rather than panicing) when misconfigured.
- `OpenFile(flag, perm)` value type added, for finer control over opening files.
- Significantly improved usage formatting.
- *2014-06-19* -- Stable v1.0.0 release.
- Support [cumulative positional](#consuming-all-remaining-arguments) arguments.
- Return error rather than panic when there are fatal errors not caught by
the type system. eg. when a default value is invalid.
- Use gokpg.in.
- *2014-06-10* -- Place-holder streamlining.
- Renamed `MetaVar` to `PlaceHolder`.
- Removed `MetaVarFromDefault`. Kingpin now uses [heuristics](#place-holders-in-help)
to determine what to display.
## Examples
### Simple Example
Kingpin can be used for simple flag+arg applications like so:
```
$ ping --help
usage: ping [<flags>] <ip> [<count>]
Flags:
--debug Enable debug mode.
--help Show help.
-t, --timeout=5s Timeout waiting for ping.
Args:
<ip> IP address to ping.
[<count>] Number of packets to send
$ ping 1.2.3.4 5
Would ping: 1.2.3.4 with timeout 5s and count 0
```
From the following source:
```go
package main
import (
"fmt"
"gopkg.in/alecthomas/kingpin.v2"
)
var (
debug = kingpin.Flag("debug", "Enable debug mode.").Bool()
timeout = kingpin.Flag("timeout", "Timeout waiting for ping.").Default("5s").OverrideDefaultFromEnvar("PING_TIMEOUT").Short('t').Duration()
ip = kingpin.Arg("ip", "IP address to ping.").Required().IP()
count = kingpin.Arg("count", "Number of packets to send").Int()
)
func main() {
kingpin.Version("0.0.1")
kingpin.Parse()
fmt.Printf("Would ping: %s with timeout %s and count %d", *ip, *timeout, *count)
}
```
### Complex Example
Kingpin can also produce complex command-line applications with global flags,
subcommands, and per-subcommand flags, like this:
```
$ chat --help
usage: chat [<flags>] <command> [<flags>] [<args> ...]
A command-line chat application.
Flags:
--help Show help.
--debug Enable debug mode.
--server=127.0.0.1 Server address.
Commands:
help [<command>]
Show help for a command.
register <nick> <name>
Register a new user.
post [<flags>] <channel> [<text>]
Post a message to a channel.
$ chat help post
usage: chat [<flags>] post [<flags>] <channel> [<text>]
Post a message to a channel.
Flags:
--image=IMAGE Image to post.
Args:
<channel> Channel to post to.
[<text>] Text to post.
$ chat post --image=~/Downloads/owls.jpg pics
...
```
From this code:
```go
package main
import (
"os"
"strings"
"gopkg.in/alecthomas/kingpin.v2"
)
var (
app = kingpin.New("chat", "A command-line chat application.")
debug = app.Flag("debug", "Enable debug mode.").Bool()
serverIP = app.Flag("server", "Server address.").Default("127.0.0.1").IP()
register = app.Command("register", "Register a new user.")
registerNick = register.Arg("nick", "Nickname for user.").Required().String()
registerName = register.Arg("name", "Name of user.").Required().String()
post = app.Command("post", "Post a message to a channel.")
postImage = post.Flag("image", "Image to post.").File()
postChannel = post.Arg("channel", "Channel to post to.").Required().String()
postText = post.Arg("text", "Text to post.").Strings()
)
func main() {
switch kingpin.MustParse(app.Parse(os.Args[1:])) {
// Register user
case register.FullCommand():
println(*registerNick)
// Post message
case post.FullCommand():
if *postImage != nil {
}
text := strings.Join(*postText, " ")
println("Post:", text)
}
}
```
## Reference Documentation
### Displaying errors and usage information
Kingpin exports a set of functions to provide consistent errors and usage
information to the user.
Error messages look something like this:
<app>: error: <message>
The functions on `Application` are:
Function | Purpose
---------|--------------
`Errorf(format, args)` | Display a printf formatted error to the user.
`Fatalf(format, args)` | As with Errorf, but also call the termination handler.
`FatalUsage(format, args)` | As with Fatalf, but also print contextual usage information.
`FatalUsageContext(context, format, args)` | As with Fatalf, but also print contextual usage information from a `ParseContext`.
`FatalIfError(err, format, args)` | Conditionally print an error prefixed with format+args, then call the termination handler
There are equivalent global functions in the kingpin namespace for the default
`kingpin.CommandLine` instance.
### Sub-commands
Kingpin supports nested sub-commands, with separate flag and positional
arguments per sub-command. Note that positional arguments may only occur after
sub-commands.
For example:
```go
var (
deleteCommand = kingpin.Command("delete", "Delete an object.")
deleteUserCommand = deleteCommand.Command("user", "Delete a user.")
deleteUserUIDFlag = deleteUserCommand.Flag("uid", "Delete user by UID rather than username.")
deleteUserUsername = deleteUserCommand.Arg("username", "Username to delete.")
deletePostCommand = deleteCommand.Command("post", "Delete a post.")
)
func main() {
switch kingpin.Parse() {
case "delete user":
case "delete post":
}
}
```
### Custom Parsers
Kingpin supports both flag and positional argument parsers for converting to
Go types. For example, some included parsers are `Int()`, `Float()`,
`Duration()` and `ExistingFile()`.
Parsers conform to Go's [`flag.Value`](http://godoc.org/flag#Value)
interface, so any existing implementations will work.
For example, a parser for accumulating HTTP header values might look like this:
```go
type HTTPHeaderValue http.Header
func (h *HTTPHeaderValue) Set(value string) error {
parts := strings.SplitN(value, ":", 2)
if len(parts) != 2 {
return fmt.Errorf("expected HEADER:VALUE got '%s'", value)
}
(*http.Header)(h).Add(parts[0], parts[1])
return nil
}
func (h *HTTPHeaderValue) String() string {
return ""
}
```
As a convenience, I would recommend something like this:
```go
func HTTPHeader(s Settings) (target *http.Header) {
target = new(http.Header)
s.SetValue((*HTTPHeaderValue)(target))
return
}
```
You would use it like so:
```go
headers = HTTPHeader(kingpin.Flag("header", "Add a HTTP header to the request.").Short('H'))
```
### Default Values
The default value is the zero value for a type. This can be overridden with
the `Default(value)` function on flags and arguments. This function accepts a
string, which is parsed by the value itself, so it *must* be compliant with
the format expected.
### Place-holders in Help
The place-holder value for a flag is the value used in the help to describe
the value of a non-boolean flag.
The value provided to PlaceHolder() is used if provided, then the value
provided by Default() if provided, then finally the capitalised flag name is
used.
Here are some examples of flags with various permutations:
--name=NAME // Flag(...).String()
--name="Harry" // Flag(...).Default("Harry").String()
--name=FULL-NAME // flag(...).PlaceHolder("FULL-NAME").Default("Harry").String()
### Consuming all remaining arguments
A common command-line idiom is to use all remaining arguments for some
purpose. eg. The following command accepts an arbitrary number of
IP addresses as positional arguments:
./cmd ping 10.1.1.1 192.168.1.1
Kingpin supports this by having `Value` provide a `IsCumulative() bool`
function. If this function exists and returns true, the value parser will be
called repeatedly for every remaining argument.
Examples of this are the `Strings()` and `StringMap()` values.
To implement the above example we might do something like this:
```go
type ipList []net.IP
func (i *ipList) Set(value string) error {
if ip := net.ParseIP(value); ip == nil {
return fmt.Errorf("'%s' is not an IP address", value)
} else {
*i = append(*i, ip)
return nil
}
}
func (i *ipList) String() string {
return ""
}
func (i *ipList) IsCumulative() bool {
return true
}
func IPList(s Settings) (target *[]net.IP) {
target = new([]net.IP)
s.SetValue((*ipList)(target))
return
}
```
And use it like so:
```go
ips := IPList(kingpin.Arg("ips", "IP addresses to ping."))
```
### Supporting -h for help
`kingpin.CommandLine.HelpFlag.Short('-h')`
### Custom help
Kingpin v2 supports templatised help using the text/template library (actually, [a fork](https://github.com/alecthomas/template)).
You can specify the template to use with the [Application.UsageTemplate()](http://godoc.org/gopkg.in/alecthomas/kingpin.v2#Application.UsageTemplate) function.
There are four included templates: `kingpin.DefaultUsageTemplate` is the default,
`kingpin.CompactUsageTemplate` provides a more compact representation for more complex command-line structures,
`kingpin.SeparateOptionalFlagsUsageTemplate` looks like the default template, but splits required
and optional command flags into separate lists, and `kingpin.ManPageTemplate` is used to generate man pages.
See the above templates for examples of usage, and the the function [UsageForContextWithTemplate()](https://github.com/alecthomas/kingpin/blob/master/usage.go#L198) method for details on the context.
#### Default help template
```
$ go run ./examples/curl/curl.go --help
usage: curl [<flags>] <command> [<args> ...]
An example implementation of curl.
Flags:
--help Show help.
-t, --timeout=5s Set connection timeout.
-H, --headers=HEADER=VALUE
Add HTTP headers to the request.
Commands:
help [<command>...]
Show help.
get url <url>
Retrieve a URL.
get file <file>
Retrieve a file.
post [<flags>] <url>
POST a resource.
```
#### Compact help template
```
$ go run ./examples/curl/curl.go --help
usage: curl [<flags>] <command> [<args> ...]
An example implementation of curl.
Flags:
--help Show help.
-t, --timeout=5s Set connection timeout.
-H, --headers=HEADER=VALUE
Add HTTP headers to the request.
Commands:
help [<command>...]
get [<flags>]
url <url>
file <file>
post [<flags>] <url>
```

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