191 Commits

Author SHA1 Message Date
孙圣翔²⁰₂₁
fdd6ff9435 fix for goreleaser 2024-12-04 08:57:52 +08:00
codeskyblue
29bd44ac0c Merge pull request #212 from black-06/master
feat: support for multiple http users
2024-12-04 08:36:52 +08:00
codeskyblue
4b9be2fd12 Merge pull request #210 from NaturezzZ/master
[feat] directly play video
2024-12-04 08:33:57 +08:00
black-06
2d7a0f1aaf feat: support for multiple http users 2024-10-28 19:20:44 +08:00
NaturezzZ
23f70c844a [feat] directly play video 2024-10-19 14:36:26 +08:00
codeskyblue
6d8af0af8f Merge pull request #202 from codeskyblue/dependabot/go_modules/golang.org/x/net-0.23.0 2024-05-14 13:55:34 +08:00
dependabot[bot]
189100b3af Bump golang.org/x/net from 0.17.0 to 0.23.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.17.0 to 0.23.0.
- [Commits](https://github.com/golang/net/compare/v0.17.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-19 12:32:20 +00:00
codeskyblue
75eb27f9c9 merge pr 2023-12-19 11:43:27 +08:00
dependabot[bot]
e31775af89 Bump golang.org/x/net from 0.7.0 to 0.17.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.7.0 to 0.17.0.
- [Commits](https://github.com/golang/net/compare/v0.7.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-19 11:41:36 +08:00
Gary Marigliano
2a0e1f7400 Add no-index option to disable indexing and search 2023-12-19 11:41:18 +08:00
dependabot[bot]
dbd0e06c73 Bump golang.org/x/net from 0.0.0-20201021035429-f5854403a974 to 0.7.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.0.0-20201021035429-f5854403a974 to 0.7.0.
- [Release notes](https://github.com/golang/net/releases)
- [Commits](https://github.com/golang/net/commits/v0.7.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-19 11:38:34 +08:00
Kushal Azim Ekram
bd2d79953d Added alpine and upx based container build. Much smaller size ~14mb 2023-12-19 11:38:34 +08:00
Jiawen Geng
dd8692b656 fix docker build
3.612 E: Failed to fetch http://deb.debian.org/debian/dists/stretch-updates/main/binary-amd64/Packages  404  Not Found [IP: 151.101.202.132 80]
3.612 E: Some index files failed to download. They have been ignored, or old ones used instead.
------
Dockerfile:10
--------------------
   8 |     WORKDIR /app
   9 |     RUN mkdir -p /app/public
  10 | >>> RUN apt-get update && apt-get install -y ca-certificates
2023-12-19 11:38:34 +08:00
codeskyblue
eacf2d1dc3 cors enable by default 2023-12-19 11:38:34 +08:00
codeskyblue
08e2cf764d limit cors only get, head, options 2023-12-19 11:38:34 +08:00
codeskyblue
a400a028ff Merge pull request #193 from codeskyblue/dependabot/go_modules/golang.org/x/net-0.17.0
Bump golang.org/x/net from 0.7.0 to 0.17.0
2023-12-19 11:17:21 +08:00
codeskyblue
f2fb06ae18 Merge pull request #174 from krypty/add-option-to-disable-index
Add no-index option to disable indexing and search
2023-12-19 11:16:48 +08:00
dependabot[bot]
71957e4947 Bump golang.org/x/net from 0.7.0 to 0.17.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.7.0 to 0.17.0.
- [Commits](https://github.com/golang/net/compare/v0.7.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-19 03:15:17 +00:00
codeskyblue
5fab10ed10 Merge pull request #170 from codeskyblue/dependabot/go_modules/golang.org/x/net-0.7.0
Bump golang.org/x/net from 0.0.0-20201021035429-f5854403a974 to 0.7.0
2023-12-19 11:14:42 +08:00
codeskyblue
30e4b86595 Merge pull request #173 from kforeverisback/alpine-small-build
Smaller Build using Alpine and UPX
2023-12-19 11:12:10 +08:00
codeskyblue
5bcbcd0bac Merge pull request #185 from gengjiawen/patch-3
fix docker build
2023-12-19 11:11:14 +08:00
codeskyblue
2a5610bb74 Merge pull request #192 from codeskyblue/add-cors
cors enable by default
2023-12-19 11:07:02 +08:00
codeskyblue
b86e0e623e cors enable by default 2023-12-19 11:05:52 +08:00
codeskyblue
bca7b842f5 Merge pull request #191 from codeskyblue/limit-cors
limit cors only get, head, options
2023-12-19 10:38:43 +08:00
codeskyblue
ea7272437d limit cors only get, head, options 2023-12-19 10:37:39 +08:00
Jiawen Geng
6de7ca4676 fix docker build
3.612 E: Failed to fetch http://deb.debian.org/debian/dists/stretch-updates/main/binary-amd64/Packages  404  Not Found [IP: 151.101.202.132 80]
3.612 E: Some index files failed to download. They have been ignored, or old ones used instead.
------
Dockerfile:10
--------------------
   8 |     WORKDIR /app
   9 |     RUN mkdir -p /app/public
  10 | >>> RUN apt-get update && apt-get install -y ca-certificates
2023-10-07 16:54:56 +08:00
Gary Marigliano
88433ea55e Add no-index option to disable indexing and search 2023-03-13 10:17:42 +01:00
Kushal Azim Ekram
d874ba59cf Added alpine and upx based container build. Much smaller size ~14mb 2023-03-11 21:16:26 -08:00
Gary Marigliano
5dc0040375 Make deep path depth configurable
Allow to configure with --deep-path-max-depth the depth of paths to combine
when listing the folders. The default is still 5.
2023-03-09 11:48:20 +01:00
dependabot[bot]
034215a031 Bump golang.org/x/net from 0.0.0-20201021035429-f5854403a974 to 0.7.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.0.0-20201021035429-f5854403a974 to 0.7.0.
- [Release notes](https://github.com/golang/net/releases)
- [Commits](https://github.com/golang/net/commits/v0.7.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-25 01:35:46 +00:00
codeskyblue
80c9e79869 update again 2022-07-26 19:23:30 +08:00
codeskyblue
2adc57be6c change ci from travis to github actions 2022-07-26 18:44:49 +08:00
codeskyblue
6829892be4 update go build version 2022-07-26 18:33:24 +08:00
codeskyblue
26545513de fix #137 2022-05-23 18:19:46 +08:00
codeskyblue
342d9cc14c Merge pull request #125 from maxunof/master
Migration to go:embed and some other changes
2021-09-14 14:22:54 +08:00
codeskyblue
ed85a9687b Merge pull request #126 from miguelnv/issue#117-error-fetching-large-folders
issue#117-error-fetching-large-folders
2021-09-13 22:29:55 +08:00
Miguel Vieira
fe8dcd76c1 thread-safe concurrent file size info map 2021-09-05 03:34:26 +01:00
Max
230bc3d182 so now we can use go install 2021-08-20 11:34:30 +00:00
Max
28a90b90dd fix version load 2021-08-20 11:26:15 +00:00
Max
e607da8ec2 make footer more pretty 2021-08-20 11:26:00 +00:00
Max
e47bdd1272 migrate from vfs to go:embed 2021-08-20 11:24:42 +00:00
Max
ff821dcb63 fix typo in readme (#122) 2021-08-20 13:18:03 +03:00
codeskyblue
8887f4ef69 Merge pull request #124 from maxunof/master
fix readme
2021-08-20 16:36:54 +08:00
Max
bd1a35541a fix readme x2 2021-08-19 17:47:15 +03:00
Max
56045c6c8e fix readme 2021-08-19 16:48:45 +03:00
codeskyblue
b5bd6779f9 fix #105 2021-04-06 10:52:25 +08:00
codeskyblue
8665999fc0 Merge branch 'prefix' 2021-04-06 10:43:01 +08:00
codeskyblue
677c5a7cec update names 2021-04-06 10:42:49 +08: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
codeskyblue
e99a12b6c3 fix some prefix error 2021-03-30 13:37:30 +08: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
5a7724d542 support prefix, need more test 2021-03-26 18:11:33 +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
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
203 changed files with 3665 additions and 27878 deletions

2
.ghs.yml Normal file
View File

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

34
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: goreleaser
on:
push:
# run only against tags
tags:
- '*'
permissions:
contents: write
# packages: write
# issues: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Fetch all tags
run: git fetch --force --tags
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.18
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
with:
# either 'goreleaser' (default) or 'goreleaser-pro'
distribution: goreleaser
version: latest
args: release
env:
GITHUB_TOKEN: ${{ secrets.TOKEN }}

6
.gitignore vendored
View File

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

32
.goreleaser.yml Normal file
View File

@@ -0,0 +1,32 @@
version: 2
before:
hooks:
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
main: .
ldflags: -s -w -X main.VERSION={{.Version}} -X main.BUILDTIME={{.Date}} -X main.GITCOMMIT={{.Commit}}
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm64
- "386"
goarm:
- "6"
archives:
- format: tar.gz
# use zip for windows archives
format_overrides:
- goos: windows
format: zip
# brews:
# - tap:
# owner: codeskyblue
# name: homebrew-tap
# folder: Formula

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,6 @@
The MIT License (MIT)
Copyright (c) 2016 shengxiang
Copyright (c) 2018 shengxiang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

262
README.md
View File

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

12
assets.go Normal file
View File

@@ -0,0 +1,12 @@
package main
import (
"embed"
"net/http"
)
//go:embed assets
var assetsFS embed.FS
// Assets contains project assets.
var Assets = http.FS(assetsFS)

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

306
assets/index.html Normal file
View File

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

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

461
assets/js/index.js Normal file
View File

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

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

49
assets/video-player.html Normal file
View File

@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Video Player - [[.FileName]]</title>
<style>
body, html {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
overflow: hidden;
background-color: #000;
}
.video-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
}
video {
max-width: 100%;
max-height: 100%;
}
h1 {
color: #fff;
font-size: 24px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="video-container">
<video id="videoPlayer" controls autoplay>
<source src="[[.VideoURL]]" type="video/[[.Extension]]">
Your browser does not support the video tag.
</video>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var video = document.getElementById('videoPlayer');
video.focus();
});
</script>
</body>
</html>

View File

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

15
docker/Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
FROM golang:1.16
WORKDIR /app/gohttpserver
ADD . /app/gohttpserver
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags '-X main.VERSION=docker' -o gohttpserver
FROM debian:stable
WORKDIR /app
RUN mkdir -p /app/public
RUN apt-get update && apt-get install -y ca-certificates
VOLUME /app/public
ADD assets ./assets
COPY --from=0 /app/gohttpserver/gohttpserver .
EXPOSE 8000
ENTRYPOINT [ "/app/gohttpserver", "--root=/app/public" ]
CMD []

19
docker/Dockerfile.alpine Normal file
View File

@@ -0,0 +1,19 @@
FROM golang:1.16.0-alpine as build
WORKDIR /app
COPY go.mod .
COPY go.sum .
RUN go mod download
# Binary Compression
RUN apk add upx --no-cache
# Build app and pack using upx
ADD . /app
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags '-s -w -X main.VERSION=docker' -o gohttpserver && upx gohttpserver
FROM alpine:3
WORKDIR /app
ADD assets /usr/local/bin/assets
COPY --from=build /app/gohttpserver /usr/local/bin/gohttpserver
EXPOSE 8000
ENTRYPOINT [ "/usr/local/bin/gohttpserver" ]

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

25
go.mod Normal file
View File

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

107
go.sum Normal file
View File

@@ -0,0 +1,107 @@
github.com/alecthomas/kingpin v2.2.6+incompatible h1:5svnBTFgJjZvGKyYBtMB0+m5wvrbUHiqye8wRJMlnYI=
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/codeskyblue/dockerignore v0.0.0-20151214070507-de82dee623d9 h1:c9axcChJwkLuSl9AvwTHi8jiBa6+VX4gGgERhABgv2E=
github.com/codeskyblue/dockerignore v0.0.0-20151214070507-de82dee623d9/go.mod h1:XNZkUhPf+qgRnhY/ecS3B73ODJ2NXCzDMJHXM069IMg=
github.com/codeskyblue/go-accesslog v0.0.0-20171215023101-6188d3bd9371 h1:dEBIvaVFaP2Uc9QA6J41qWxE5NfEnDWEBk+kWv5nK5k=
github.com/codeskyblue/go-accesslog v0.0.0-20171215023101-6188d3bd9371/go.mod h1:sgXnVxxZ1u72GAzc9s1SzpuPMxBDKfTg6F2PvDrPSJU=
github.com/codeskyblue/openid-go v0.0.0-20160923065855-0d30842b2fb4 h1:66lzN78lwccK+BPztRgBiWCYzhlerQEVOh2oeBksu5I=
github.com/codeskyblue/openid-go v0.0.0-20160923065855-0d30842b2fb4/go.mod h1:K/hSCtAHvnE9aM+LsYgVmgzPNFuWFdx6i9t6/3jNrZQ=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fork2fix/go-plist v0.0.0-20181126021357-36960be5e636 h1:ESUdS2eb8LyDQfboYyFBwAL+rqYhnTZ15ntw8BLsd9g=
github.com/fork2fix/go-plist v0.0.0-20181126021357-36960be5e636/go.mod h1:v6KRhgoO1QKamoeuZ7yHqZIP8p6j9k41Tb0jCyOEmr4=
github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d h1:lBXNCxVENCipq4D1Is42JVOP4eQjlB8TQ6H69Yx5J9Q=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/handlers v1.4.0 h1:XulKRWSQK5uChr4pEgSE4Tc/OcmnU9GJuSwdog/tZsA=
github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/shogo82148/androidbinary v0.0.0-20180627093851-01c4bfa8b3b5 h1:bXRaUWl3Afe3F9YR5NU1U3UB5zjCHlu4im5p3J/LUYk=
github.com/shogo82148/androidbinary v0.0.0-20180627093851-01c4bfa8b3b5/go.mod h1:05AjXWPWLdTIl9+REKhSmTeoJ6Wz5e9ir0Q0NRxCIKo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
howett.net/plist v0.0.0-20201203080718-1454fab16a06 h1:QDxUo/w2COstK1wIBYpzQlHX/NqaQTcf9jyz347nI58=
howett.net/plist v0.0.0-20201203080718-1454fab16a06/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=

View File

@@ -3,6 +3,9 @@ package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"html/template"
"io"
"io/ioutil"
"log"
@@ -13,35 +16,61 @@ import (
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"regexp"
"github.com/go-yaml/yaml"
"github.com/gorilla/mux"
"github.com/shogo82148/androidbinary/apk"
)
const YAMLCONF = ".ghs.yml"
type ApkInfo struct {
PackageName string `json:"packageName"`
MainActivity string `json:"mainActivity"`
Version struct {
Code int `json:"code"`
Name string `json:"name"`
} `json:"version"`
}
type IndexFileItem struct {
Path string
Info os.FileInfo
}
type Directory struct {
size map[string]int64
mutex *sync.RWMutex
}
type HTTPStaticServer struct {
Root string
Upload bool
Delete bool
Title string
Theme string
PlistProxy string
GoogleTrackerId string
Root string
Prefix string
Upload bool
Delete bool
Title string
Theme string
PlistProxy string
GoogleTrackerID string
AuthType string
DeepPathMaxDepth int
NoIndex bool
indexes []IndexFileItem
m *mux.Router
bufPool sync.Pool // use sync.Pool caching buf to reduce gc ratio
}
func NewHTTPStaticServer(root string) *HTTPStaticServer {
if root == "" {
root = "./"
}
root = filepath.ToSlash(root)
func NewHTTPStaticServer(root string, noIndex bool) *HTTPStaticServer {
// if root == "" {
// root = "./"
// }
// root = filepath.ToSlash(root)
root = filepath.ToSlash(filepath.Clean(root))
if !strings.HasSuffix(root, "/") {
root = root + "/"
}
@@ -51,31 +80,33 @@ func NewHTTPStaticServer(root string) *HTTPStaticServer {
Root: root,
Theme: "black",
m: m,
bufPool: sync.Pool{
New: func() interface{} { return make([]byte, 32*1024) },
},
NoIndex: noIndex,
}
go func() {
time.Sleep(1 * time.Second)
for {
startTime := time.Now()
log.Println("Started making search index")
s.makeIndex()
log.Printf("Completed search index in %v", time.Since(startTime))
//time.Sleep(time.Second * 1)
time.Sleep(time.Minute * 10)
}
}()
if !noIndex {
go func() {
time.Sleep(1 * time.Second)
for {
startTime := time.Now()
log.Println("Started making search index")
s.makeIndex()
log.Printf("Completed search index in %v", time.Since(startTime))
//time.Sleep(time.Second * 1)
time.Sleep(time.Minute * 10)
}
}()
}
m.HandleFunc("/-/status", s.hStatus)
m.HandleFunc("/-/zip/{path:.*}", s.hZip)
m.HandleFunc("/-/unzip/{zip_path:.*}/-/{path:.*}", s.hUnzip)
m.HandleFunc("/-/json/{path:.*}", s.hJSONList)
// routers for Apple *.ipa
m.HandleFunc("/-/ipa/plist/{path:.*}", s.hPlist)
m.HandleFunc("/-/ipa/link/{path:.*}", s.hIpaLink)
// TODO: /ipa/info
m.HandleFunc("/-/video-player/{path:.*}", s.hVideoPlayer)
m.HandleFunc("/{path:.*}", s.hIndex).Methods("GET")
m.HandleFunc("/{path:.*}", s.hUpload).Methods("POST")
m.HandleFunc("/{path:.*}", s.hIndex).Methods("GET", "HEAD")
m.HandleFunc("/{path:.*}", s.hUploadOrMkdir).Methods("POST")
m.HandleFunc("/{path:.*}", s.hDelete).Methods("DELETE")
return s
}
@@ -84,91 +115,249 @@ func (s *HTTPStaticServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.m.ServeHTTP(w, r)
}
// Return real path with Seperator(/)
func (s *HTTPStaticServer) getRealPath(r *http.Request) string {
path := mux.Vars(r)["path"]
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
path = filepath.Clean(path) // prevent .. for safe issues
relativePath, err := filepath.Rel(s.Prefix, path)
if err != nil {
relativePath = path
}
realPath := filepath.Join(s.Root, relativePath)
return filepath.ToSlash(realPath)
}
func (s *HTTPStaticServer) hIndex(w http.ResponseWriter, r *http.Request) {
path := mux.Vars(r)["path"]
relPath := filepath.Join(s.Root, path)
realPath := s.getRealPath(r)
if r.FormValue("json") == "true" {
s.hJSONList(w, r)
return
}
finfo, err := os.Stat(relPath)
if err == nil && finfo.IsDir() {
tmpl.ExecuteTemplate(w, "index", s)
if r.FormValue("op") == "info" {
s.hInfo(w, r)
return
}
if r.FormValue("op") == "archive" {
s.hZip(w, r)
return
}
log.Println("GET", path, realPath)
if r.FormValue("raw") == "false" || isDir(realPath) {
if r.Method == "HEAD" {
return
}
renderHTML(w, "assets/index.html", s)
} else {
if filepath.Base(path) == YAMLCONF {
auth := s.readAccessConf(realPath)
if !auth.Delete {
http.Error(w, "Security warning, not allowed to read", http.StatusForbidden)
return
}
}
if r.FormValue("download") == "true" {
w.Header().Set("Content-Disposition", "attachment; filename="+strconv.Quote(filepath.Base(path)))
}
http.ServeFile(w, r, relPath)
http.ServeFile(w, r, realPath)
}
}
func (s *HTTPStaticServer) hStatus(w http.ResponseWriter, r *http.Request) {
data, _ := json.MarshalIndent(s, "", " ")
w.Header().Set("Content-Type", "application/json")
w.Write(data)
}
func (s *HTTPStaticServer) hDelete(w http.ResponseWriter, req *http.Request) {
// only can delete file now
path := mux.Vars(req)["path"]
auth := s.readAccessConf(path)
log.Printf("%#v", auth)
if !auth.Delete {
realPath := s.getRealPath(req)
// path = filepath.Clean(path) // for safe reason, prevent path contain ..
auth := s.readAccessConf(realPath)
if !auth.canDelete(req) {
http.Error(w, "Delete forbidden", http.StatusForbidden)
return
}
err := os.Remove(filepath.Join(s.Root, path))
// TODO: path safe check
err := os.RemoveAll(realPath)
if err != nil {
http.Error(w, err.Error(), 500)
pathErr, ok := err.(*os.PathError)
if ok {
http.Error(w, pathErr.Op+" "+path+": "+pathErr.Err.Error(), 500)
} else {
http.Error(w, err.Error(), 500)
}
return
}
w.Write([]byte("Success"))
}
func (s *HTTPStaticServer) hUpload(w http.ResponseWriter, req *http.Request) {
path := mux.Vars(req)["path"]
func (s *HTTPStaticServer) hUploadOrMkdir(w http.ResponseWriter, req *http.Request) {
dirpath := s.getRealPath(req)
// check auth
auth := s.readAccessConf(path)
if !auth.Upload {
auth := s.readAccessConf(dirpath)
if !auth.canUpload(req) {
http.Error(w, "Upload forbidden", http.StatusForbidden)
return
}
err := req.ParseMultipartForm(1 << 30) // max memory 1G
file, header, err := req.FormFile("file")
if _, err := os.Stat(dirpath); os.IsNotExist(err) {
if err := os.MkdirAll(dirpath, os.ModePerm); err != nil {
log.Println("Create directory:", err)
http.Error(w, "Directory create "+err.Error(), http.StatusInternalServerError)
return
}
}
if file == nil { // only mkdir
w.Header().Set("Content-Type", "application/json;charset=utf-8")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"destination": dirpath,
})
return
}
if err != nil {
log.Println("Parse form file:", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if len(req.MultipartForm.File["file"]) == 0 {
http.Error(w, "Need multipart file", http.StatusInternalServerError)
defer func() {
file.Close()
req.MultipartForm.RemoveAll() // Seen from go source code, req.MultipartForm not nil after call FormFile(..)
}()
filename := req.FormValue("filename")
if filename == "" {
filename = header.Filename
}
if err := checkFilename(filename); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
dirpath := filepath.Join(s.Root, path)
dstPath := filepath.Join(dirpath, filename)
for _, mfile := range req.MultipartForm.File["file"] {
file, err := mfile.Open()
defer file.Close()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
dst, err := os.Create(filepath.Join(dirpath, mfile.Filename)) // BUG(ssx): There is a leak here
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer dst.Close()
if _, err := io.Copy(dst, file); err != nil {
log.Println("Handle upload file:", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Large file (>32MB) will store in tmp directory
// The quickest operation is call os.Move instead of os.Copy
// Note: it seems not working well, os.Rename might be failed
var copyErr error
// if osFile, ok := file.(*os.File); ok && fileExists(osFile.Name()) {
// tmpUploadPath := osFile.Name()
// osFile.Close() // Windows can not rename opened file
// log.Printf("Move %s -> %s", tmpUploadPath, dstPath)
// copyErr = os.Rename(tmpUploadPath, dstPath)
// } else {
dst, err := os.Create(dstPath)
if err != nil {
log.Println("Create file:", err)
http.Error(w, "File create "+err.Error(), http.StatusInternalServerError)
return
}
w.Write([]byte("Upload success"))
// Note: very large size file might cause poor performance
// _, copyErr = io.Copy(dst, file)
buf := s.bufPool.Get().([]byte)
defer s.bufPool.Put(buf)
_, copyErr = io.CopyBuffer(dst, file, buf)
dst.Close()
// }
if copyErr != nil {
log.Println("Handle upload file:", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json;charset=utf-8")
if req.FormValue("unzip") == "true" {
err = unzipFile(dstPath, dirpath)
os.Remove(dstPath)
message := "success"
if err != nil {
message = err.Error()
}
json.NewEncoder(w).Encode(map[string]interface{}{
"success": err == nil,
"description": message,
})
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"destination": dstPath,
})
}
type FileJSONInfo struct {
Name string `json:"name"`
Type string `json:"type"`
Size int64 `json:"size"`
Path string `json:"path"`
ModTime int64 `json:"mtime"`
Extra interface{} `json:"extra,omitempty"`
}
// path should be absolute
func parseApkInfo(path string) (ai *ApkInfo) {
defer func() {
if err := recover(); err != nil {
log.Println("parse-apk-info panic:", err)
}
}()
apkf, err := apk.OpenFile(path)
if err != nil {
return
}
ai = &ApkInfo{}
ai.MainActivity, _ = apkf.MainActivity()
ai.PackageName = apkf.PackageName()
ai.Version.Code = apkf.Manifest().VersionCode
ai.Version.Name = apkf.Manifest().VersionName
return
}
func (s *HTTPStaticServer) hInfo(w http.ResponseWriter, r *http.Request) {
path := mux.Vars(r)["path"]
relPath := s.getRealPath(r)
fi, err := os.Stat(relPath)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
fji := &FileJSONInfo{
Name: fi.Name(),
Size: fi.Size(),
Path: path,
ModTime: fi.ModTime().UnixNano() / 1e6,
}
ext := filepath.Ext(path)
switch ext {
case ".md":
fji.Type = "markdown"
case ".apk":
fji.Type = "apk"
fji.Extra = parseApkInfo(relPath)
case "":
fji.Type = "dir"
default:
fji.Type = "text"
}
data, _ := json.Marshal(fji)
w.Header().Set("Content-Type", "application/json")
w.Write(data)
}
func (s *HTTPStaticServer) hZip(w http.ResponseWriter, r *http.Request) {
path := mux.Vars(r)["path"]
CompressToZip(w, filepath.Join(s.Root, path))
CompressToZip(w, s.getRealPath(r))
}
func (s *HTTPStaticServer) hUnzip(w http.ResponseWriter, r *http.Request) {
@@ -185,13 +374,9 @@ func (s *HTTPStaticServer) hUnzip(w http.ResponseWriter, r *http.Request) {
}
}
func genURLStr(r *http.Request, path string) *url.URL {
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
func combineURL(r *http.Request, path string) *url.URL {
return &url.URL{
Scheme: scheme,
Scheme: r.URL.Scheme,
Host: r.Host,
Path: path,
}
@@ -204,7 +389,7 @@ func (s *HTTPStaticServer) hPlist(w http.ResponseWriter, r *http.Request) {
path = path[0:len(path)-6] + ".ipa"
}
relPath := filepath.Join(s.Root, path)
relPath := s.getRealPath(r)
plinfo, err := parseIPA(relPath)
if err != nil {
http.Error(w, err.Error(), 500)
@@ -230,9 +415,11 @@ func (s *HTTPStaticServer) hPlist(w http.ResponseWriter, r *http.Request) {
func (s *HTTPStaticServer) hIpaLink(w http.ResponseWriter, r *http.Request) {
path := mux.Vars(r)["path"]
plistUrl := genURLStr(r, "/-/ipa/plist/"+path).String()
if r.TLS == nil {
// send plist to plistproxy and get a https link
var plistUrl string
if r.URL.Scheme == "https" {
plistUrl = combineURL(r, "/-/ipa/plist/"+path).String()
} else if s.PlistProxy != "" {
httpPlistLink := "http://" + r.Host + "/-/ipa/plist/" + path
url, err := s.genPlistLink(httpPlistLink)
if err != nil {
@@ -240,17 +427,17 @@ func (s *HTTPStaticServer) hIpaLink(w http.ResponseWriter, r *http.Request) {
return
}
plistUrl = url
//plistUrl = strings.TrimSuffix(s.PlistProxy, "/") + "/" + r.Host + "/-/ipa/plist/" + path
} else {
http.Error(w, "500: Server should be https:// or provide valid plistproxy", 500)
return
}
w.Header().Set("Content-Type", "text/html")
tmpl.ExecuteTemplate(w, "ipa-install", map[string]string{
log.Println("PlistURL:", plistUrl)
renderHTML(w, "assets/ipa-install.html", map[string]string{
"Name": filepath.Base(path),
"PlistLink": plistUrl,
})
// w.Write([]byte(fmt.Sprintf(
// `<a href='itms-services://?action=download-manifest&url=%s'>Click this link to install</a>`,
// plistUrl)))
}
func (s *HTTPStaticServer) genPlistLink(httpPlistLink string) (plistUrl string, err error) {
@@ -282,27 +469,115 @@ func (s *HTTPStaticServer) genPlistLink(httpPlistLink string) (plistUrl string,
}
func (s *HTTPStaticServer) hFileOrDirectory(w http.ResponseWriter, r *http.Request) {
path := mux.Vars(r)["path"]
http.ServeFile(w, r, filepath.Join(s.Root, path))
http.ServeFile(w, r, s.getRealPath(r))
}
type ListResponse struct {
type HTTPFileInfo struct {
Name string `json:"name"`
Path string `json:"path"`
Type string `json:"type"`
Size string `json:"size"`
Size int64 `json:"size"`
ModTime int64 `json:"mtime"`
}
type AccessTable struct {
Regex string `yaml:"regex"`
Allow bool `yaml:"allow"`
}
type UserControl struct {
Email string
// Access bool
Upload bool
Delete bool
Token string
}
type AccessConf struct {
Upload bool `yaml:"upload" json:"upload"`
Delete bool `yaml:"delete" json:"delete"`
Upload bool `yaml:"upload" json:"upload"`
Delete bool `yaml:"delete" json:"delete"`
Users []UserControl `yaml:"users" json:"users"`
AccessTables []AccessTable `yaml:"accessTables"`
}
var reCache = make(map[string]*regexp.Regexp)
func (c *AccessConf) canAccess(fileName string) bool {
for _, table := range c.AccessTables {
pattern, ok := reCache[table.Regex]
if !ok {
pattern, _ = regexp.Compile(table.Regex)
reCache[table.Regex] = pattern
}
// skip wrong format regex
if pattern == nil {
continue
}
if pattern.MatchString(fileName) {
return table.Allow
}
}
return true
}
func (c *AccessConf) canDelete(r *http.Request) bool {
session, err := store.Get(r, defaultSessionName)
if err != nil {
return c.Delete
}
val := session.Values["user"]
if val == nil {
return c.Delete
}
userInfo := val.(*UserInfo)
for _, rule := range c.Users {
if rule.Email == userInfo.Email {
return rule.Delete
}
}
return c.Delete
}
func (c *AccessConf) canUploadByToken(token string) bool {
for _, rule := range c.Users {
if rule.Token == token {
return rule.Upload
}
}
return c.Upload
}
func (c *AccessConf) canUpload(r *http.Request) bool {
token := r.FormValue("token")
if token != "" {
return c.canUploadByToken(token)
}
session, err := store.Get(r, defaultSessionName)
if err != nil {
return c.Upload
}
val := session.Values["user"]
if val == nil {
return c.Upload
}
userInfo := val.(*UserInfo)
for _, rule := range c.Users {
if rule.Email == userInfo.Email {
return rule.Upload
}
}
return c.Upload
}
func (s *HTTPStaticServer) hJSONList(w http.ResponseWriter, r *http.Request) {
requestPath := mux.Vars(r)["path"]
localPath := filepath.Join(s.Root, requestPath)
realPath := s.getRealPath(r)
search := r.FormValue("search")
auth := s.readAccessConf(realPath)
auth.Upload = auth.canUpload(r)
auth.Delete = auth.canDelete(r)
maxDepth := s.DeepPathMaxDepth
// path string -> info os.FileInfo
fileInfoMap := make(map[string]os.FileInfo, 0)
@@ -318,7 +593,7 @@ func (s *HTTPStaticServer) hJSONList(w http.ResponseWriter, r *http.Request) {
}
}
} else {
infos, err := ioutil.ReadDir(localPath)
infos, err := ioutil.ReadDir(realPath)
if err != nil {
http.Error(w, err.Error(), 500)
return
@@ -329,9 +604,12 @@ func (s *HTTPStaticServer) hJSONList(w http.ResponseWriter, r *http.Request) {
}
// turn file list -> json
lrs := make([]ListResponse, 0)
lrs := make([]HTTPFileInfo, 0)
for path, info := range fileInfoMap {
lr := ListResponse{
if !auth.canAccess(info.Name()) {
continue
}
lr := HTTPFileInfo{
Name: info.Name(),
Path: path,
ModTime: info.ModTime().UnixNano() / 1e6,
@@ -344,35 +622,41 @@ func (s *HTTPStaticServer) hJSONList(w http.ResponseWriter, r *http.Request) {
lr.Name = filepath.ToSlash(name) // fix for windows
}
if info.IsDir() {
name := deepPath(localPath, info.Name())
name := deepPath(realPath, info.Name(), maxDepth)
lr.Name = name
lr.Path = filepath.Join(filepath.Dir(path), name)
lr.Type = "dir"
lr.Size = "-"
lr.Size = s.historyDirSize(lr.Path)
} else {
lr.Type = "file"
lr.Size = formatSize(info)
lr.Size = info.Size() // formatSize(info)
}
lrs = append(lrs, lr)
}
data, _ := json.Marshal(map[string]interface{}{
"files": lrs,
"auth": s.readAccessConf(requestPath),
"auth": auth,
})
w.Header().Set("Content-Type", "application/json")
w.Write(data)
}
var dirInfoSize = Directory{size: make(map[string]int64), mutex: &sync.RWMutex{}}
func (s *HTTPStaticServer) makeIndex() error {
var indexes = make([]IndexFileItem, 0)
var err = filepath.Walk(s.Root, func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Printf("WARN: Visit path: %s error: %v", strconv.Quote(path), err)
return filepath.SkipDir
// return err
}
if info.IsDir() {
return nil
}
if filepath.IsAbs(path) {
path, _ = filepath.Rel(s.Root, path)
}
path, _ = filepath.Rel(s.Root, path)
path = filepath.ToSlash(path)
indexes = append(indexes, IndexFileItem{path, info})
return nil
@@ -381,6 +665,28 @@ func (s *HTTPStaticServer) makeIndex() error {
return err
}
func (s *HTTPStaticServer) historyDirSize(dir string) int64 {
dirInfoSize.mutex.RLock()
size, ok := dirInfoSize.size[dir]
dirInfoSize.mutex.RUnlock()
if ok {
return size
}
for _, fitem := range s.indexes {
if filepath.HasPrefix(fitem.Path, dir) {
size += fitem.Info.Size()
}
}
dirInfoSize.mutex.Lock()
dirInfoSize.size[dir] = size
dirInfoSize.mutex.Unlock()
return size
}
func (s *HTTPStaticServer) findIndex(text string) []IndexFileItem {
ret := make([]IndexFileItem, 0)
for _, item := range s.indexes {
@@ -410,16 +716,23 @@ func (s *HTTPStaticServer) findIndex(text string) []IndexFileItem {
func (s *HTTPStaticServer) defaultAccessConf() AccessConf {
return AccessConf{
Upload: s.Upload,
Delete: s.Delete,
}
}
func (s *HTTPStaticServer) readAccessConf(requestPath string) (ac AccessConf) {
ac = s.defaultAccessConf()
relPath := filepath.Join(s.Root, requestPath)
if isFile(relPath) {
relPath = filepath.Dir(relPath)
func (s *HTTPStaticServer) readAccessConf(realPath string) (ac AccessConf) {
relativePath, err := filepath.Rel(s.Root, realPath)
if err != nil || relativePath == "." || relativePath == "" { // actually relativePath is always "." if root == realPath
ac = s.defaultAccessConf()
realPath = s.Root
} else {
parentPath := filepath.Dir(realPath)
ac = s.readAccessConf(parentPath)
}
cfgFile := filepath.Join(relPath, ".ghs.yml")
if isFile(realPath) {
realPath = filepath.Dir(realPath)
}
cfgFile := filepath.Join(realPath, YAMLCONF)
data, err := ioutil.ReadFile(cfgFile)
if err != nil {
if os.IsNotExist(err) {
@@ -434,11 +747,9 @@ func (s *HTTPStaticServer) readAccessConf(requestPath string) (ac AccessConf) {
return
}
func deepPath(basedir, name string) string {
isDir := true
func deepPath(basedir, name string, maxDepth int) string {
// loop max 5, incase of for loop not finished
maxDepth := 5
for depth := 0; depth <= maxDepth && isDir; depth += 1 {
for depth := 0; depth <= maxDepth; depth += 1 {
finfos, err := ioutil.ReadDir(filepath.Join(basedir, name))
if err != nil || len(finfos) != 1 {
break
@@ -452,7 +763,82 @@ func deepPath(basedir, name string) string {
return name
}
func isFile(path string) bool {
info, err := os.Stat(path)
return err == nil && info.Mode().IsRegular()
func assetsContent(name string) string {
fd, err := Assets.Open(name)
if err != nil {
panic(err)
}
data, err := ioutil.ReadAll(fd)
if err != nil {
panic(err)
}
return string(data)
}
// TODO: I need to read more abouthtml/template
var (
funcMap template.FuncMap
)
func init() {
funcMap = template.FuncMap{
"title": strings.Title,
"urlhash": func(path string) string {
httpFile, err := Assets.Open(path)
if err != nil {
return path + "#no-such-file"
}
info, err := httpFile.Stat()
if err != nil {
return path + "#stat-error"
}
return fmt.Sprintf("%s?t=%d", path, info.ModTime().Unix())
},
}
}
var (
_tmpls = make(map[string]*template.Template)
)
func renderHTML(w http.ResponseWriter, name string, v interface{}) {
if t, ok := _tmpls[name]; ok {
t.Execute(w, v)
return
}
t := template.Must(template.New(name).Funcs(funcMap).Delims("[[", "]]").Parse(assetsContent(name)))
_tmpls[name] = t
t.Execute(w, v)
}
func checkFilename(name string) error {
if strings.ContainsAny(name, "\\/:*<>|") {
return errors.New("Name should not contains \\/:*<>|")
}
return nil
}
func (s *HTTPStaticServer) hVideoPlayer(w http.ResponseWriter, r *http.Request) {
path := mux.Vars(r)["path"]
realPath := s.getRealPath(r)
extension := strings.ToLower(strings.TrimPrefix(filepath.Ext(path), "."))
if _, err := os.Stat(realPath); os.IsNotExist(err) {
http.Error(w, "File not found", http.StatusNotFound)
return
}
fileName := filepath.Base(path)
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
videoURL := fmt.Sprintf("%s://%s/%s", scheme, r.Host, path)
renderHTML(w, "assets/video-player.html", map[string]interface{}{
"FileName": fileName,
"VideoURL": videoURL,
"Extension": extension,
})
}

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

172
main.go
View File

@@ -2,53 +2,69 @@ package main
import (
"bytes"
"crypto/sha256"
"crypto/subtle"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
"os"
"regexp"
"runtime"
"strconv"
"strings"
"text/template"
"github.com/alecthomas/kingpin"
accesslog "github.com/codeskyblue/go-accesslog"
"github.com/go-yaml/yaml"
"github.com/goji/httpauth"
"github.com/gorilla/handlers"
accesslog "github.com/mash/go-accesslog"
"github.com/gorilla/mux"
)
type Configure struct {
Conf *os.File `yaml:"-"`
Addr string `yaml:"addr"`
Port int `yaml:"port"`
Root string `yaml:"root"`
HttpAuth string `yaml:"httpauth"`
Prefix string `yaml:"prefix"`
HTTPAuth string `yaml:"httpauth"`
Cert string `yaml:"cert"`
Key string `yaml:"key"`
Cors bool `yaml:"cors"`
Theme string `yaml:"theme"`
XHeaders bool `yaml:"xheaders"`
Upload bool `yaml:"upload"`
Delete bool `yaml:"delete"`
PlistProxy string `yaml:"plistproxy"`
Title string `yaml:"title"`
Debug bool `yaml:"debug"`
GoogleTrackerId string `yaml:"google-tracker-id"`
GoogleTrackerID string `yaml:"google-tracker-id"`
Auth struct {
Type string `yaml:"type"` // openid|http|github
OpenID string `yaml:"openid"`
HTTP []string `yaml:"http"`
ID string `yaml:"id"` // for oauth2
Secret string `yaml:"secret"` // for oauth2
} `yaml:"auth"`
DeepPathMaxDepth int `yaml:"deep-path-max-depth"`
NoIndex bool `yaml:"no-index"`
}
type logger struct {
}
type httpLogger struct{}
func (l logger) Log(record accesslog.LogRecord) {
func (l httpLogger) Log(record accesslog.LogRecord) {
log.Printf("%s - %s %d %s", record.Ip, record.Method, record.Status, record.Uri)
}
var (
defaultPlistProxy = "https://plistproxy.herokuapp.com/plist"
defaultOpenID = "https://login.netease.com/openid"
gcfg = Configure{}
l = logger{}
logger = httpLogger{}
VERSION = "unknown"
BUILDTIME = "unknown time"
@@ -79,28 +95,38 @@ func versionMessage() string {
func parseFlags() error {
// initial default conf
gcfg.Root = "./"
gcfg.Addr = ":8000"
gcfg.Port = 8000
gcfg.Addr = ""
gcfg.Theme = "black"
gcfg.PlistProxy = defaultPlistProxy
gcfg.GoogleTrackerId = "UA-81205425-2"
gcfg.Auth.OpenID = defaultOpenID
gcfg.GoogleTrackerID = "UA-81205425-2"
gcfg.Title = "Go HTTP File Server"
gcfg.DeepPathMaxDepth = 5
gcfg.NoIndex = false
kingpin.HelpFlag.Short('h')
kingpin.Version(versionMessage())
kingpin.Flag("conf", "config file path, yaml format").FileVar(&gcfg.Conf)
kingpin.Flag("root", "root directory, default ./").Short('r').StringVar(&gcfg.Root)
kingpin.Flag("addr", "listen address, default :8000").Short('a').StringVar(&gcfg.Addr)
kingpin.Flag("prefix", "url prefix, eg /foo").StringVar(&gcfg.Prefix)
kingpin.Flag("port", "listen port, default 8000").IntVar(&gcfg.Port)
kingpin.Flag("addr", "listen address, eg 127.0.0.1:8000").Short('a').StringVar(&gcfg.Addr)
kingpin.Flag("cert", "tls cert.pem path").StringVar(&gcfg.Cert)
kingpin.Flag("key", "tls key.pem path").StringVar(&gcfg.Key)
kingpin.Flag("httpauth", "HTTP basic auth (ex: user:pass)").StringVar(&gcfg.HttpAuth)
kingpin.Flag("auth-type", "Auth type <http|openid>").StringVar(&gcfg.Auth.Type)
kingpin.Flag("auth-http", "HTTP basic auth (ex: user:pass)").StringsVar(&gcfg.Auth.HTTP)
kingpin.Flag("auth-openid", "OpenID auth identity url").StringVar(&gcfg.Auth.OpenID)
kingpin.Flag("theme", "web theme, one of <black|green>").StringVar(&gcfg.Theme)
kingpin.Flag("upload", "enable upload support").BoolVar(&gcfg.Upload)
kingpin.Flag("delete", "enable delete support").BoolVar(&gcfg.Delete)
kingpin.Flag("xheaders", "used when behide nginx").BoolVar(&gcfg.XHeaders)
kingpin.Flag("cors", "enable cross-site HTTP request").BoolVar(&gcfg.Cors)
kingpin.Flag("debug", "enable debug mode").BoolVar(&gcfg.Debug)
kingpin.Flag("plistproxy", "plist proxy when server is not https").Short('p').StringVar(&gcfg.PlistProxy)
kingpin.Flag("title", "server title").StringVar(&gcfg.Title)
kingpin.Flag("google-tracker-id", "set to empty to disable it").StringVar(&gcfg.GoogleTrackerId)
kingpin.Flag("google-tracker-id", "set to empty to disable it").StringVar(&gcfg.GoogleTrackerID)
kingpin.Flag("deep-path-max-depth", "set to -1 to not combine dirs").IntVar(&gcfg.DeepPathMaxDepth)
kingpin.Flag("no-index", "disable indexing").BoolVar(&gcfg.NoIndex)
kingpin.Parse() // first parse conf
@@ -117,6 +143,52 @@ func parseFlags() error {
return nil
}
func fixPrefix(prefix string) string {
prefix = regexp.MustCompile(`/*$`).ReplaceAllString(prefix, "")
if !strings.HasPrefix(prefix, "/") {
prefix = "/" + prefix
}
if prefix == "/" {
prefix = ""
}
return prefix
}
func cors(next http.Handler) http.Handler {
// access control and CORS middleware
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "*")
w.Header().Set("Access-Control-Allow-Headers", "*")
if r.Method == "OPTIONS" {
return
}
next.ServeHTTP(w, r)
})
}
func multiBasicAuth(auths []string) func(http.Handler) http.Handler {
userPassMap := make(map[string]string)
for _, auth := range auths {
userpass := strings.SplitN(auth, ":", 2)
if len(userpass) == 2 {
userPassMap[userpass[0]] = userpass[1]
}
}
return httpauth.BasicAuth(httpauth.AuthOptions{
Realm: "Restricted",
AuthFunc: func(user, pass string, request *http.Request) bool {
password, ok := userPassMap[user]
if !ok {
return false
}
givenPass := sha256.Sum256([]byte(pass))
requiredPass := sha256.Sum256([]byte(password))
return subtle.ConstantTimeCompare(givenPass[:], requiredPass[:]) == 1
},
})
}
func main() {
if err := parseFlags(); err != nil {
log.Fatal(err)
@@ -125,12 +197,23 @@ func main() {
data, _ := yaml.Marshal(gcfg)
fmt.Printf("--- config ---\n%s\n", string(data))
}
log.SetFlags(log.Lshortfile | log.LstdFlags)
ss := NewHTTPStaticServer(gcfg.Root)
// make sure prefix matches: ^/.*[^/]$
gcfg.Prefix = fixPrefix(gcfg.Prefix)
if gcfg.Prefix != "" {
log.Printf("url prefix: %s", gcfg.Prefix)
}
ss := NewHTTPStaticServer(gcfg.Root, gcfg.NoIndex)
ss.Prefix = gcfg.Prefix
ss.Theme = gcfg.Theme
ss.Title = gcfg.Title
ss.GoogleTrackerId = gcfg.GoogleTrackerId
ss.GoogleTrackerID = gcfg.GoogleTrackerID
ss.Upload = gcfg.Upload
ss.Delete = gcfg.Delete
ss.AuthType = gcfg.Auth.Type
ss.DeepPathMaxDepth = gcfg.DeepPathMaxDepth
if gcfg.PlistProxy != "" {
u, err := url.Parse(gcfg.PlistProxy)
@@ -140,44 +223,73 @@ func main() {
u.Scheme = "https"
ss.PlistProxy = u.String()
}
if ss.PlistProxy != "" {
log.Printf("plistproxy: %s", strconv.Quote(ss.PlistProxy))
}
var hdlr http.Handler = ss
hdlr = accesslog.NewLoggingHandler(hdlr, l)
hdlr = accesslog.NewLoggingHandler(hdlr, logger)
// HTTP Basic Authentication
userpass := strings.SplitN(gcfg.HttpAuth, ":", 2)
if len(userpass) == 2 {
user, pass := userpass[0], userpass[1]
hdlr = httpauth.SimpleBasicAuth(user, pass)(hdlr)
switch gcfg.Auth.Type {
case "http":
hdlr = multiBasicAuth(gcfg.Auth.HTTP)(hdlr)
case "openid":
handleOpenID(gcfg.Auth.OpenID, false) // FIXME(ssx): set secure default to false
// case "github":
// handleOAuth2ID(gcfg.Auth.Type, gcfg.Auth.ID, gcfg.Auth.Secret) // FIXME(ssx): set secure default to false
case "oauth2-proxy":
handleOauth2()
}
// CORS
if gcfg.Cors {
hdlr = handlers.CORS()(hdlr)
}
hdlr = cors(hdlr)
if gcfg.XHeaders {
hdlr = handlers.ProxyHeaders(hdlr)
}
http.Handle("/", hdlr)
http.HandleFunc("/-/sysinfo", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
mainRouter := mux.NewRouter()
router := mainRouter
if gcfg.Prefix != "" {
router = mainRouter.PathPrefix(gcfg.Prefix).Subrouter()
mainRouter.Handle(gcfg.Prefix, hdlr)
mainRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, gcfg.Prefix, http.StatusTemporaryRedirect)
})
}
router.PathPrefix("/-/assets/").Handler(http.StripPrefix(gcfg.Prefix+"/-/", http.FileServer(Assets)))
router.HandleFunc("/-/sysinfo", func(w http.ResponseWriter, r *http.Request) {
data, _ := json.Marshal(map[string]interface{}{
"version": VERSION,
})
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(data)))
w.Write(data)
})
router.PathPrefix("/").Handler(hdlr)
if gcfg.Addr == "" {
gcfg.Addr = fmt.Sprintf(":%d", gcfg.Port)
}
if !strings.Contains(gcfg.Addr, ":") {
gcfg.Addr = ":" + gcfg.Addr
}
log.Printf("listening on %s\n", strconv.Quote(gcfg.Addr))
_, port, _ := net.SplitHostPort(gcfg.Addr)
log.Printf("listening on %s, local address http://%s:%s\n", strconv.Quote(gcfg.Addr), getLocalIP(), port)
srv := &http.Server{
Handler: mainRouter,
Addr: gcfg.Addr,
}
var err error
if gcfg.Key != "" && gcfg.Cert != "" {
err = http.ListenAndServeTLS(gcfg.Addr, gcfg.Cert, gcfg.Key, nil)
err = srv.ListenAndServeTLS(gcfg.Cert, gcfg.Key)
} else {
err = http.ListenAndServe(gcfg.Addr, nil)
err = srv.ListenAndServe()
}
log.Fatal(err)
}

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

1
testdata/filetypes/script.js vendored Normal file
View File

@@ -0,0 +1 @@
document.write("Hello world!")

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,28 @@
package main
import (
"fmt"
"net"
"net/http"
"os"
"strconv"
"path/filepath"
"strings"
)
func formatSize(file os.FileInfo) string {
if file.IsDir() {
return "-"
}
size := file.Size()
switch {
case size > 1024*1024:
return fmt.Sprintf("%.1f MB", float64(size)/1024/1024)
case size > 1024:
return fmt.Sprintf("%.1f KB", float64(size)/1024)
default:
return strconv.Itoa(int(size)) + " B"
}
return ""
}
// func formatSize(file os.FileInfo) string {
// if file.IsDir() {
// return "-"
// }
// size := file.Size()
// switch {
// case size > 1024*1024:
// return fmt.Sprintf("%.1f MB", float64(size)/1024/1024)
// case size > 1024:
// return fmt.Sprintf("%.1f KB", float64(size)/1024)
// default:
// return strconv.Itoa(int(size)) + " B"
// }
// return ""
// }
func getRealIP(req *http.Request) string {
xip := req.Header.Get("X-Real-IP")
@@ -56,3 +56,43 @@ func SublimeContains(s, substr string) bool {
}
return ok
}
// getLocalIP returns the non loopback local IP of the host
func getLocalIP() string {
addrs, err := net.InterfaceAddrs()
if err != nil {
return ""
}
for _, address := range addrs {
// check the address type and if it is not a loopback the display it
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
return ipnet.IP.String()
}
}
}
return ""
}
func fileExists(path string) bool {
info, err := os.Stat(path)
if err != nil {
return false
}
return !info.IsDir()
}
// Convert path to normal paths
func cleanPath(path string) string {
return filepath.ToSlash(filepath.Clean(path))
}
func isFile(path string) bool {
info, err := os.Stat(path)
return err == nil && info.Mode().IsRegular()
}
func isDir(path string) bool {
info, err := os.Stat(path)
return err == nil && info.Mode().IsDir()
}

View File

@@ -25,3 +25,25 @@ func TestSublimeContains(t *testing.T) {
}
}
}
func TestCleanPath(t *testing.T) {
tests := []struct {
orig string
expect string
}{
// {"C:\\hello", "C:/hello"}, // Only works in windows
{"", "."},
{"//../foo", "/foo"},
{"/../../", "/"},
{"/hello/world/..", "/hello"},
{"/..", "/"},
{"/foo/..", "/"},
{"/-/foo", "/-/foo"},
}
for _, v := range tests {
res := cleanPath(v.orig)
if res != v.expect {
t.Fatalf("Clean path(%v) expect(%v) but got(%v)", v.orig, v.expect, res)
}
}
}

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
}

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