Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3aaf8f5db | ||
|
|
26cd70cbaa | ||
|
|
65aae5ec95 | ||
|
|
5af4332593 | ||
|
|
12717f15cc | ||
|
|
dc1009cdd8 | ||
|
|
52bda5f823 | ||
|
|
44b6932202 | ||
|
|
0556d8b69d | ||
|
|
01516e030b | ||
|
|
c32d68809b | ||
|
|
d48172986e | ||
|
|
e6f6dbbddd | ||
|
|
7995a588bd | ||
|
|
baa587166c | ||
|
|
8dab66567d | ||
|
|
a7959ad0fa | ||
|
|
25304f8229 | ||
|
|
bc0602c523 | ||
|
|
ae60604ef1 | ||
|
|
4bdcfbd5b1 | ||
|
|
53c02d8914 | ||
|
|
4d74052ad8 | ||
|
|
435a67cb5d | ||
|
|
aadee6ac8a | ||
|
|
9c07ac356b | ||
|
|
9a3c79d93d | ||
|
|
2bd9f39da5 | ||
|
|
c4e63dcc85 | ||
|
|
794e5a22c1 | ||
|
|
eeb5245cb9 | ||
|
|
e8328b25bc | ||
|
|
9c1ce91de1 | ||
|
|
073bcea56f | ||
|
|
9e3872c980 | ||
|
|
658d14314b | ||
|
|
107a3ac6d4 | ||
|
|
ad82841eb2 | ||
|
|
c9d94b54d7 | ||
|
|
d96ddf9b18 | ||
|
|
7917c2ad35 | ||
|
|
2db5ce544f | ||
|
|
474b99c6d5 | ||
|
|
aded63f76f | ||
|
|
d1a7e6ecfe | ||
|
|
a5b8e02a98 | ||
|
|
72af00f1b7 | ||
|
|
b86a849e00 | ||
|
|
09c3a1862a | ||
|
|
54da27ef70 | ||
|
|
d9728cbb7a | ||
|
|
420478ce6e | ||
|
|
21d2893292 | ||
|
|
1189bdef33 | ||
|
|
15d2e7917a | ||
|
|
903a265676 | ||
|
|
965cf754cf | ||
|
|
3850eedf07 | ||
|
|
0b400b1ffd | ||
|
|
abb143601b | ||
|
|
4d193f0dd6 | ||
|
|
9ca959c31c | ||
|
|
29d4b276e2 | ||
|
|
6ed3d5b501 | ||
|
|
14037ddad2 | ||
|
|
1ecf18e41c | ||
|
|
badcf700b9 | ||
|
|
527c680e1f | ||
|
|
174b2ffdbb | ||
|
|
b4e2f2f13e | ||
|
|
fb7676ab4e | ||
|
|
c9018e843e | ||
|
|
31db9658e0 | ||
|
|
449254bcfd | ||
|
|
fc380df8dd | ||
|
|
dc4e8598f7 | ||
|
|
b0151ee985 | ||
|
|
b9eba7bfa4 | ||
|
|
cad6c39e72 | ||
|
|
2e64b56d14 | ||
|
|
1620f2be56 | ||
|
|
f6fa44b7fc | ||
|
|
ab33472bf4 | ||
|
|
5ad15326ec | ||
|
|
86c301bcf9 | ||
|
|
7a52df61ec | ||
|
|
0c40f1907e | ||
|
|
35641e5957 | ||
|
|
82130ecce0 | ||
|
|
ad81f666a5 | ||
|
|
847f47c5d3 | ||
|
|
aefcf81bb3 | ||
|
|
e3b2d08021 | ||
|
|
3da2af9111 | ||
|
|
a6eade1ae1 | ||
|
|
3b3d739e91 | ||
|
|
9bb3afe6ce | ||
|
|
41a99be4ca | ||
|
|
808f6eea33 | ||
|
|
974570c79e | ||
|
|
5ba53d3cbd | ||
|
|
8748f07167 | ||
|
|
d603b293d8 | ||
|
|
2ce5e9a1e2 | ||
|
|
108754a8c3 | ||
|
|
8ea8960696 | ||
|
|
1cbcb73948 | ||
|
|
e68c7e2c61 | ||
|
|
e09f892442 | ||
|
|
56f3b45a4e | ||
|
|
77770112bd | ||
|
|
b18af0b907 | ||
|
|
da98261d27 | ||
|
|
9fae7c06af | ||
|
|
b29c013706 | ||
|
|
0b095f43bb | ||
|
|
e2be689a05 | ||
|
|
7dce0fb4f4 | ||
|
|
a66cf92bd5 | ||
|
|
5c8bdf04a1 | ||
|
|
315bf650d4 | ||
|
|
f2579a7b37 | ||
|
|
319b88cedf | ||
|
|
9af721176b | ||
|
|
113e2f27fb | ||
|
|
7b3e99b599 | ||
|
|
2398035e53 | ||
|
|
d153d47604 | ||
|
|
a02c813784 | ||
|
|
d72f1c85c1 | ||
|
|
62fc8a1054 | ||
|
|
e4b1c60ce5 | ||
|
|
b620d8f0f2 | ||
|
|
47500bf92e | ||
|
|
6938eb5f35 | ||
|
|
8936adbe1c | ||
|
|
121f278713 | ||
|
|
0a0b07a255 | ||
|
|
f44b436bc1 | ||
|
|
75ab487031 | ||
|
|
133c936d7b | ||
|
|
b0146a5a76 | ||
|
|
9c0d887a3c | ||
|
|
2b2c76bde9 | ||
|
|
43fe84de3a | ||
|
|
318c33e8ff | ||
|
|
c32784e8ae | ||
|
|
898cdd2d65 | ||
|
|
b441151e09 | ||
|
|
0ea173ffe5 | ||
|
|
4b1d0a614c | ||
|
|
02d598f406 | ||
|
|
ae11e469d2 | ||
|
|
4f2fac11d3 | ||
|
|
68bc907bd2 | ||
|
|
d7e5805024 | ||
|
|
97e31b6c47 | ||
|
|
cb67ef8258 | ||
|
|
dc3d7303d7 | ||
|
|
cdb74c6a19 | ||
|
|
cf432e32f7 | ||
|
|
c3435e2ece | ||
|
|
b52394a166 | ||
|
|
d65f8eb0f5 | ||
|
|
86acd842b0 | ||
|
|
69b89a0c2a | ||
|
|
2992dd43e8 | ||
|
|
cb4ecb49eb | ||
|
|
d51294ce54 | ||
|
|
32eb35f7b2 | ||
|
|
80717e6c69 | ||
|
|
d123f8fc07 | ||
|
|
2dcaf4a3a1 | ||
|
|
c4cdacc2d8 | ||
|
|
06afa786ef | ||
|
|
b8fccab33a | ||
|
|
ec5a12b0f7 | ||
|
|
7b6c5f74d4 | ||
|
|
02df9b519f | ||
|
|
38a3f01868 | ||
|
|
a120b58dc2 | ||
|
|
33be6b036d | ||
|
|
93211cf073 | ||
|
|
046022221e | ||
|
|
fc01b1db81 | ||
|
|
ed7557c5da | ||
|
|
5d110da392 | ||
|
|
ab443f58d0 | ||
|
|
d4dde3fe4f | ||
|
|
3067b06bf6 | ||
|
|
0e2926cc85 | ||
|
|
72b9eb443c | ||
|
|
4ad70d5392 | ||
|
|
d8ce8231cc | ||
|
|
7381ff8589 | ||
|
|
277e2d6cd8 | ||
|
|
58ae69de0b | ||
|
|
f73f812cf1 | ||
|
|
d1eba4a089 | ||
|
|
502c13c3ce | ||
|
|
ee9878ac25 | ||
|
|
4cacfc253c | ||
|
|
b6167ce422 | ||
|
|
b1dd4b5308 | ||
|
|
e7a7e02626 | ||
|
|
5e22795828 | ||
|
|
bf423658af | ||
|
|
8bd6f08794 | ||
|
|
62d88121cd | ||
|
|
85e38c390c | ||
|
|
10b6e69dec | ||
|
|
5d1cc211b0 | ||
|
|
83598f4013 | ||
|
|
76eaa3bc4b | ||
|
|
92ec968d5e | ||
|
|
049aa4ec44 | ||
|
|
869fd8c6bc | ||
|
|
d175b0af34 | ||
|
|
0aff72059b | ||
|
|
168b33d5d5 | ||
|
|
ccb84f585a | ||
|
|
39e7b3f615 | ||
|
|
73816ff2d7 | ||
|
|
f5f595ac02 | ||
|
|
c0a4e7add1 | ||
|
|
26ea9fc1b8 | ||
|
|
3c515fcef3 | ||
|
|
e771d6f49e | ||
|
|
003a661c08 | ||
|
|
28b0840e69 | ||
|
|
81af2a9a27 | ||
|
|
206416889c | ||
|
|
6ab30ed308 | ||
|
|
3a9bb58646 | ||
|
|
af7e504e4f | ||
|
|
2178a507c5 | ||
|
|
7832b5b8b8 | ||
|
|
d786d0018e | ||
|
|
a9efb88fe2 | ||
|
|
e44e66a8e8 | ||
|
|
7d81998577 | ||
|
|
5e748d0cef | ||
|
|
4f90561c41 | ||
|
|
dc57d5d15f | ||
|
|
2d03aa02e2 | ||
|
|
8d4d564935 | ||
|
|
e710dbf720 | ||
|
|
aba81334a5 | ||
|
|
c53c974713 | ||
|
|
574f642200 | ||
|
|
fd6e4cc189 | ||
|
|
c8b90d1286 | ||
|
|
cbf50299ad | ||
|
|
26b2cdc194 | ||
|
|
00ee7084a6 | ||
|
|
75dc1c1548 | ||
|
|
943963f760 | ||
|
|
03d3c35a86 | ||
|
|
3ed194b7bc | ||
|
|
c764d53b88 | ||
|
|
ccc5eddf92 | ||
|
|
1ff2f7d7ba | ||
|
|
c3f518e5b7 | ||
|
|
84cfef8480 | ||
|
|
bf14066aba | ||
|
|
c592d0063d | ||
|
|
e9787440fb | ||
|
|
1ee02d1451 | ||
|
|
2378ba894d | ||
|
|
0fd652f716 | ||
|
|
c4a33206ce | ||
|
|
6289e479a5 | ||
|
|
7d97f6424b | ||
|
|
383d4049e0 | ||
|
|
5fc844b840 | ||
|
|
4833a76b05 | ||
|
|
f594dd77fd | ||
|
|
f99afad7f6 | ||
|
|
1c327ac252 | ||
|
|
7556726e08 | ||
|
|
92729f26df | ||
|
|
701e536a90 | ||
|
|
1f45a95710 | ||
|
|
9f7f83cfd2 | ||
|
|
36fd4871e1 | ||
|
|
7882abea35 | ||
|
|
67fbadabf2 | ||
|
|
a4f2553fbc | ||
|
|
4704a8a234 | ||
|
|
ee547c5899 | ||
|
|
9b1d915dd1 | ||
|
|
f6f40ab707 | ||
|
|
33f3e20849 | ||
|
|
4fcb946f0f | ||
|
|
12976024b7 | ||
|
|
1f7367edb1 | ||
|
|
0493d77f46 | ||
|
|
4af979ac9b | ||
|
|
c28695fa18 | ||
|
|
5ffd6d612b | ||
|
|
577edd86f4 | ||
|
|
412089948c | ||
|
|
65773364a1 | ||
|
|
91bc5d0d5e | ||
|
|
b580b2456a | ||
|
|
7e27cfe5f8 | ||
|
|
4609a3d5f3 | ||
|
|
20a5646a53 | ||
|
|
0510634db7 | ||
|
|
59018287de | ||
|
|
e1edc5b550 | ||
|
|
d74bc06cbd | ||
|
|
fedde804e3 | ||
|
|
7467553bf2 | ||
|
|
9f8f45db5a | ||
|
|
b1fdbf714b | ||
|
|
68db934507 | ||
|
|
1b4902f196 | ||
|
|
61dfe67ca8 | ||
|
|
038cb620c6 | ||
|
|
0e88e3e1f4 | ||
|
|
f4fb5744ef | ||
|
|
474c439076 | ||
|
|
9cb75ec608 | ||
|
|
560308fd31 | ||
|
|
da9bdb8dcd | ||
|
|
b3723e260f | ||
|
|
c6bc631327 | ||
|
|
ec013488ff | ||
|
|
58e5609b59 | ||
|
|
00c2b8262b | ||
|
|
172f9a82d0 | ||
|
|
72325a6cbc | ||
|
|
a3c341b5b4 | ||
|
|
7d08017c7f | ||
|
|
07ee8448e6 | ||
|
|
20da36416c | ||
|
|
97b612c1b3 | ||
|
|
01a69b9bf9 | ||
|
|
d9cb536518 | ||
|
|
887acd5a55 | ||
|
|
da0916b8c7 | ||
|
|
8f90ec4173 | ||
|
|
2d2bd1d0b1 | ||
|
|
661fed0dbb | ||
|
|
e3e1d7b012 | ||
|
|
1e88d3dc5f | ||
|
|
b943f5a51d | ||
|
|
5b8937ac3a | ||
|
|
d0d84a62cd | ||
|
|
7aa7b5526b | ||
|
|
812aff9686 | ||
|
|
23e6b02ae4 | ||
|
|
2f69538576 | ||
|
|
e52782fe9c | ||
|
|
376d2283bf | ||
|
|
3c3c61fee2 | ||
|
|
b6a7d8a54a | ||
|
|
9f4c803495 | ||
|
|
d4baaaa0ff | ||
|
|
f3bc35ad35 | ||
|
|
ae8b87fb18 | ||
|
|
f0406cac8e | ||
|
|
12dcc45e88 | ||
|
|
1f39c2e229 | ||
|
|
9430d734a7 | ||
|
|
2ca5463d1d | ||
|
|
a9fe99b09b | ||
|
|
dd71d1c49e | ||
|
|
ee93fbb120 | ||
|
|
ce96579d26 | ||
|
|
3ae6392a82 | ||
|
|
ac16a94a92 | ||
|
|
65f5bb2daf | ||
|
|
22810a96b7 | ||
|
|
638fd6234e | ||
|
|
838b275c19 | ||
|
|
6945843c97 | ||
|
|
6d53606b22 |
+10
-6
@@ -1,4 +1,6 @@
|
||||
sudo: required
|
||||
dist: trusty
|
||||
group: deprecated-2017Q4
|
||||
|
||||
language: go
|
||||
|
||||
@@ -6,14 +8,15 @@ services:
|
||||
- docker
|
||||
|
||||
addons:
|
||||
postgresql: "9.4"
|
||||
postgresql: "9.6"
|
||||
|
||||
go:
|
||||
- 1.5.4
|
||||
- 1.6.2
|
||||
- 1.10.x
|
||||
- 1.11.x
|
||||
|
||||
env:
|
||||
- GO15VENDOREXPERIMENT=1
|
||||
before_install:
|
||||
- ./script/check_formatting.sh
|
||||
- ./script/check_assets.sh
|
||||
|
||||
install:
|
||||
- make setup
|
||||
@@ -21,4 +24,5 @@ install:
|
||||
script:
|
||||
- make build
|
||||
- make test
|
||||
- PGHOST=127.0.0.1 ./script/test_all.sh
|
||||
- make test-all
|
||||
- make docker-release
|
||||
|
||||
+127
@@ -1,3 +1,130 @@
|
||||
## 0.11.1 - 2019-01-28
|
||||
|
||||
- Typo fixes
|
||||
- Add Base64 javascript encoder/decoder to replace deprecated window.atob call, GH-405
|
||||
- Fix startup error when DATABASE_URL is set, GH-406
|
||||
- Fix user auto detection when USER env var is not set, GH-408
|
||||
- Switch bindata dependency to use maintained fork: github.com/go-bindata/go-bindata, GH-409
|
||||
|
||||
## 0.11.0 - 2018-12-24
|
||||
|
||||
- Tweak sidebar database object counters styles, GH-400
|
||||
- Do not exit with error if local server is not running, GH-399
|
||||
- Fix SSH host verification check, GH-398
|
||||
- Scope activity list to current database only, GH-397
|
||||
- Show current release version and check for updates, GH-396
|
||||
- Force switch back to default connection settings view, GH-395
|
||||
- Fix row count estimation bug, GH-394
|
||||
- Print out failed query SQL and args with --debug flag, GH-393
|
||||
|
||||
## 0.10.0 - 2018-11-28
|
||||
|
||||
- Fixes relation not found errors when dealing with table names that have uppercase characters, GH-356
|
||||
- Dockerfile updates, GH-357
|
||||
- Check if pg_dump is available before running database export, GH-358
|
||||
- Improvements to CockroachDB integration, GH-365
|
||||
- Add EstimatedTableRowsCount to avoid count in large tables, GH-366
|
||||
- Automatically set table filter option to 'equals' if its not set, GH-370
|
||||
- Dependencies update and switch to dep, GH-375
|
||||
- Add column context menu item to get numeric stats, GH-377
|
||||
- Fix issues with connection string builder, GH-378
|
||||
- Include rows count to numeric stats view on table column, GH-379
|
||||
- Make localhost to be a default db host, GH-380
|
||||
- Clear out connection settings/bookmark on login screen when running in session/connect mode
|
||||
- Add table row context menu with actions, GH-381
|
||||
- Allow settings url prefix with URL_PREFIX env var, GH-387
|
||||
- Fix JSON marshal panic when dealing with NaN values, GH-388
|
||||
- Fix startup behavior when user did not provide a database name, GH-389
|
||||
|
||||
## 0.9.12 - 2018-04-23
|
||||
|
||||
- Add link to view database connection string format on login page
|
||||
- Include constraint name under "constraints" tab, GH-343
|
||||
- Misc CI and config changes
|
||||
|
||||
## 0.9.11 - 2017-12-07
|
||||
|
||||
- Fix ssl mode for the connection url in the bookmarks, GH-320
|
||||
- Add support for CORS, GH-321
|
||||
- Fix custom query results counter for empty queries, GH-322
|
||||
- Reorganize the table context menu, GH-323
|
||||
- Disable database connection string text field autocomplete, GH-327
|
||||
- Add db prefix to the table export files, GH-329
|
||||
- Add database view context menu with export actions, GH-330
|
||||
|
||||
## 0.9.10 - 2017-11-03
|
||||
|
||||
- Make idle connection timeout configurable, [GH-282]
|
||||
- Fix panics when sshinfo is not set on bookmarks, [GH-296]
|
||||
- Dot now allow using startup bookmark in multi-session mode, [GH-300]
|
||||
- Add ability to copy table name from the sidebar, [GH-301]
|
||||
|
||||
## 0.9.9 - 2017-09-28
|
||||
|
||||
- Automatically format JSON data exports, GH-255
|
||||
- Update Docker image to alpine:3.6, GH-256
|
||||
- Print out PostgreSQL server version on start in a single-session mode, GH-264
|
||||
- Record last query timestamp for the client connection, GH-265
|
||||
- Add context menu for table headers in browse mode (copy name, see unique values), GH-268
|
||||
- Add ability to export current database dump, GH-270
|
||||
- Automatically open pgweb in browser on start if its already running, GH-272
|
||||
- Connect to the database with credentials provided by a third-party backend, GH-266
|
||||
- Automatically close idle sessions (no activity in 1 hour), GH-275
|
||||
- Allow connecting via SSH with a custom private key and other fixes, GH-277
|
||||
- Add options to disable SSH connections, GH-279
|
||||
|
||||
## 0.9.8 - 2017-08-04
|
||||
|
||||
- Fixed error checking in the API, GH-234
|
||||
- Fixed activity tab to support PG 9.x versions, GH-237
|
||||
- Remember sort column and order for pagination, GH-240
|
||||
- Use `sslmode=disable` for bookmarks without sslmode option, GH-244
|
||||
- Javascript fixes for IE9-11, GH-245
|
||||
- Require confirmation for the disconnect, GH-246
|
||||
- Clean the results table on manual disconnect
|
||||
|
||||
## 0.9.7 - 2017-04-04
|
||||
|
||||
- Fixed issue with locked session and empty db url, GH-206
|
||||
- Fixed path rewrite on DB change, GH-212
|
||||
- Upgraded dependencies, GH-217
|
||||
- Added ability to specify bookmarks path, GH-218
|
||||
- Added counter for the number of rows from a custom SQL query, GH-224
|
||||
- Added new behavior for removing table rows view on custom SQL query page, GH-225
|
||||
|
||||
## 0.9.6 - 2016-11-18
|
||||
|
||||
- Fixed bug in query base64-encoding, GH-186
|
||||
- Fixed rows pagination visibility bug, GH-190
|
||||
- Fixed issue with query order escaping, GH-191
|
||||
- Fixed invalid query selection for explain command, GH-198
|
||||
- Fixed issue with empty sidebar, now it shows empty state, GH-202
|
||||
- Added new flag --readonly to enable read only transaction mode, GH-193
|
||||
- Added ability to kill any running query, GH-194
|
||||
- Added session database connection locking, GH-195
|
||||
- Added ability to switch between databases, GH-196
|
||||
- Added feature to keep last selected tab when switching between tables, GH-197
|
||||
- Added new flag --bookmark (-b) to specify server connection from bookmark, GH-201
|
||||
|
||||
## 0.9.5 - 2016-10-01
|
||||
|
||||
- Only view schema with USAGE privileges, GH-167
|
||||
- Fixed broken export to CSV/JSON/XML if hashmark in URL, GH-175
|
||||
- Added example service configuration for systemd, GH-177
|
||||
- Allow setting auth user and pass using variables
|
||||
|
||||
## 0.9.4 - 2016-07-29
|
||||
|
||||
- Fixes CSV/JSON/XML export buttons when pgweb is running with url prefix, GH-170
|
||||
|
||||
## 0.9.3 - 2016-06-30
|
||||
|
||||
- Uses Go 1.6 for development, GH-155
|
||||
- Fixes timestamp formatting in CSV export, GH-163
|
||||
- Included PostgreSQL 9.6 for integration testing
|
||||
- Switches docker image to Alpine to reduce image size
|
||||
- Adds support for ARMv5
|
||||
|
||||
## 0.9.2 - 2016-03-01
|
||||
|
||||
- Fixes bug with unsafe base64 encoded sql queries
|
||||
|
||||
+2
-1
@@ -2,5 +2,6 @@
|
||||
- Create a new git branch
|
||||
- Make changes
|
||||
- Run tests: `make test`
|
||||
- Run tests agains all supported PostreSQL versions: `make test-all` (optional)
|
||||
- If you change frontend code (js/css) make sure to rebuild assets: `make assets`
|
||||
- Open a PR
|
||||
- Open a new pull request
|
||||
+7
-7
@@ -1,17 +1,17 @@
|
||||
FROM alpine:3.3
|
||||
MAINTAINER Dan Sosedoff <dan.sosedoff@gmail.com>
|
||||
|
||||
ENV PGWEB_VERSION 0.9.2
|
||||
FROM alpine:3.6
|
||||
LABEL maintainer="Dan Sosedoff <dan.sosedoff@gmail.com>"
|
||||
ENV PGWEB_VERSION 0.11.1
|
||||
|
||||
RUN \
|
||||
apk update && \
|
||||
apk add ca-certificates && \
|
||||
apk add --no-cache ca-certificates openssl postgresql wget && \
|
||||
update-ca-certificates && \
|
||||
rm -rf /var/cache/apk/* && \
|
||||
cd /tmp && \
|
||||
wget https://github.com/sosedoff/pgweb/releases/download/v$PGWEB_VERSION/pgweb_linux_amd64.zip && \
|
||||
wget -q https://github.com/sosedoff/pgweb/releases/download/v$PGWEB_VERSION/pgweb_linux_amd64.zip && \
|
||||
unzip pgweb_linux_amd64.zip -d /usr/bin && \
|
||||
mv /usr/bin/pgweb_linux_amd64 /usr/bin/pgweb && \
|
||||
rm -f pgweb_linux_amd64.zip
|
||||
|
||||
EXPOSE 8081
|
||||
CMD ["/usr/bin/pgweb", "--bind=0.0.0.0", "--listen=8081"]
|
||||
CMD ["/usr/bin/pgweb", "--bind=0.0.0.0", "--listen=8081"]
|
||||
|
||||
Generated
-85
@@ -1,85 +0,0 @@
|
||||
{
|
||||
"ImportPath": "github.com/sosedoff/pgweb",
|
||||
"GoVersion": "go1.6",
|
||||
"GodepVersion": "v71",
|
||||
"Packages": [
|
||||
"./..."
|
||||
],
|
||||
"Deps": [
|
||||
{
|
||||
"ImportPath": "github.com/BurntSushi/toml",
|
||||
"Comment": "v0.1.0-9-g3883ac1",
|
||||
"Rev": "3883ac1ce943878302255f538fce319d23226223"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/davecgh/go-spew/spew",
|
||||
"Rev": "5215b55f46b2b919f50a1df0eaa5886afe4e3b3d"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/gin-gonic/gin",
|
||||
"Comment": "v0.5",
|
||||
"Rev": "50e7e427300a7eeeb299a4cd5bc1ebfa04d48528"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/gin-gonic/gin/binding",
|
||||
"Comment": "v0.5",
|
||||
"Rev": "50e7e427300a7eeeb299a4cd5bc1ebfa04d48528"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/gin-gonic/gin/render",
|
||||
"Comment": "v0.5",
|
||||
"Rev": "50e7e427300a7eeeb299a4cd5bc1ebfa04d48528"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/jessevdk/go-flags",
|
||||
"Comment": "v1-285-g1679536",
|
||||
"Rev": "1679536dcc895411a9f5848d9a0250be7856448c"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/jmoiron/sqlx",
|
||||
"Comment": "sqlx-v1.0-61-gb468c08",
|
||||
"Rev": "b468c08552f4efac78b94708eb040170a8184c47"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/jmoiron/sqlx/reflectx",
|
||||
"Comment": "sqlx-v1.0-61-gb468c08",
|
||||
"Rev": "b468c08552f4efac78b94708eb040170a8184c47"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/julienschmidt/httprouter",
|
||||
"Comment": "v1-37-gb55664b",
|
||||
"Rev": "b55664b9e92004aebb7f19a19a9d06271f3a41fc"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/lib/pq",
|
||||
"Comment": "go1.0-cutoff-56-gdc50b6a",
|
||||
"Rev": "dc50b6ad2d3ee836442cf3389009c7cd1e64bb43"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/lib/pq/oid",
|
||||
"Comment": "go1.0-cutoff-56-gdc50b6a",
|
||||
"Rev": "dc50b6ad2d3ee836442cf3389009c7cd1e64bb43"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/mitchellh/go-homedir",
|
||||
"Rev": "7d2d8c8a4e078ce3c58736ab521a40b37a504c52"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/pmezard/go-difflib/difflib",
|
||||
"Rev": "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/stretchr/testify/assert",
|
||||
"Comment": "v1.1.3",
|
||||
"Rev": "f390dcf405f7b83c997eac1b06768bb9f44dec18"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/crypto/curve25519",
|
||||
"Rev": "3760e016850398b85094c4c99e955b8c3dea5711"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/crypto/ssh",
|
||||
"Rev": "3760e016850398b85094c4c99e955b8c3dea5711"
|
||||
}
|
||||
]
|
||||
}
|
||||
Generated
-5
@@ -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.
|
||||
Generated
+186
@@ -0,0 +1,186 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
digest = "1:b16fbfbcc20645cb419f78325bb2e85ec729b338e996a228124d68931a6f2a37"
|
||||
name = "github.com/BurntSushi/toml"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "b26d9c308763d68093482582cea63d69be07a0f0"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec"
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
pruneopts = "UT"
|
||||
revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:36fe9527deed01d2a317617e59304eb2c4ce9f8a24115bcc5c2e37b3aee5bae4"
|
||||
name = "github.com/gin-contrib/sse"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:d5083934eb25e45d17f72ffa86cae3814f4a9d6c073c4f16b64147169b245606"
|
||||
name = "github.com/gin-gonic/gin"
|
||||
packages = [
|
||||
".",
|
||||
"binding",
|
||||
"json",
|
||||
"render",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "b869fe1415e4b9eb52f247441830d502aece2d4d"
|
||||
version = "v1.3.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:6ba96a683441984156b05568b9d31dbc846d3336d21ac220fcc819a367dc1f65"
|
||||
name = "github.com/golang/protobuf"
|
||||
packages = ["proto"]
|
||||
pruneopts = "UT"
|
||||
revision = "5a0f697c9ed9d68fef0116532c6e05cfeae00e55"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:a2cff208d4759f6ba1b1cd228587b0a1869f95f22542ec9cd17fff64430113c7"
|
||||
name = "github.com/jessevdk/go-flags"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "c6ca198ec95c841fdb89fc0de7496fed11ab854e"
|
||||
version = "v1.4.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:7654989089e5bd5b6734ec3be8b695e87d3f1f8d95620b343fd7d3995a5b60d7"
|
||||
name = "github.com/jmoiron/sqlx"
|
||||
packages = [
|
||||
".",
|
||||
"reflectx",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "0dae4fefe7c0e190f7b5a78dac28a1c82cc8d849"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:be97e109f627d3ba8edfef50c9c74f0d0c17cbe3a2e924a8985e4804a894f282"
|
||||
name = "github.com/json-iterator/go"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "36b14963da70d11297d313183d7e6388c8510e1e"
|
||||
version = "1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:8ef506fc2bb9ced9b151dafa592d4046063d744c646c1bbe801982ce87e4bc24"
|
||||
name = "github.com/lib/pq"
|
||||
packages = [
|
||||
".",
|
||||
"oid",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "4ded0e9383f75c197b3a2aaa6d590ac52df6fd79"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:fa610f9fe6a93f4a75e64c83673dfff9bf1a34bbb21e6102021b6bc7850834a3"
|
||||
name = "github.com/mattn/go-isatty"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "57fdcb988a5c543893cc61bce354a6e24ab70022"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:78bbb1ba5b7c3f2ed0ea1eab57bdd3859aec7e177811563edc41198a760b06af"
|
||||
name = "github.com/mitchellh/go-homedir"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "ae18d6b8b3205b561c79e8e5f69bff09736185f4"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe"
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
packages = ["difflib"]
|
||||
pruneopts = "UT"
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:18752d0b95816a1b777505a97f71c7467a8445b8ffb55631a7bf779f6ba4fa83"
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = ["assert"]
|
||||
pruneopts = "UT"
|
||||
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
|
||||
version = "v1.2.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:5589bfcc5b74ddf0a0cf48a9159f3965ec036434d520aadde91bdf95a95c40a0"
|
||||
name = "github.com/tuvistavie/securerandom"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "15512123a948d62f6361bd84818e11f2ad84059a"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c268acaa4a4d94a467980e5e91452eb61c460145765293dc0aed48e5e9919cc6"
|
||||
name = "github.com/ugorji/go"
|
||||
packages = ["codec"]
|
||||
pruneopts = "UT"
|
||||
revision = "c88ee250d0221a57af388746f5cf03768c21d6e2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:dec72d8441e4e88bbf27cc98e28e962e065ebd11bcd42063f0444020b7249618"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = [
|
||||
"curve25519",
|
||||
"ed25519",
|
||||
"ed25519/internal/edwards25519",
|
||||
"internal/chacha20",
|
||||
"internal/subtle",
|
||||
"poly1305",
|
||||
"ssh",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "182538f80094b6a8efaade63a8fd8e0d9d5843dd"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:4b487c782bc804d994e91adbd3d2a8a77a482671efd87b2fde0805adb01a39c0"
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix"]
|
||||
pruneopts = "UT"
|
||||
revision = "fa5fdf94c78965f1aa8423f0cc50b8b8d728b05a"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:1b4724d3c8125f6044925f02b485b74bfec9905cbf579d95aafd1a6c8f8447d3"
|
||||
name = "gopkg.in/go-playground/validator.v8"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "5f57d2222ad794d0dffb07e664ea05e2ee07d60c"
|
||||
version = "v8.18.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:cacb98d52c60c337c2ce95a7af83ba0313a93ce5e73fa9e99a96aff70776b9d3"
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "a5b47d31c556af34a302ce5d659e6fea44d90de0"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
input-imports = [
|
||||
"github.com/BurntSushi/toml",
|
||||
"github.com/gin-gonic/gin",
|
||||
"github.com/jessevdk/go-flags",
|
||||
"github.com/jmoiron/sqlx",
|
||||
"github.com/lib/pq",
|
||||
"github.com/mitchellh/go-homedir",
|
||||
"github.com/stretchr/testify/assert",
|
||||
"github.com/tuvistavie/securerandom",
|
||||
"golang.org/x/crypto/ssh",
|
||||
]
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
#
|
||||
# [prune]
|
||||
# non-go = false
|
||||
# go-tests = true
|
||||
# unused-packages = true
|
||||
|
||||
[metadata.heroku]
|
||||
root-package = "github.com/sosedoff/pgweb"
|
||||
go-version = "1.11"
|
||||
install = [ "./..." ]
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/BurntSushi/toml"
|
||||
version = "0.3.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gin-gonic/gin"
|
||||
version = "1.3.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/jessevdk/go-flags"
|
||||
version = "1.4.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/jmoiron/sqlx"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/lib/pq"
|
||||
version = "1.0.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/mitchellh/go-homedir"
|
||||
version = "1.0.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "1.2.2"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/tuvistavie/securerandom"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2016 Dan Sosedoff <dan.sosedoff@gmail.com>
|
||||
Copyright (c) 2014-2019 Dan Sosedoff <dan.sosedoff@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
|
||||
@@ -11,7 +11,6 @@ usage:
|
||||
@echo "----------------- : -------------------"
|
||||
@echo "make setup : Install all necessary dependencies"
|
||||
@echo "make dev : Generate development build"
|
||||
@echo "make test : Run tests"
|
||||
@echo "make build : Generate production build for current OS"
|
||||
@echo "make bootstrap : Install cross-compilation toolchain"
|
||||
@echo "make release : Generate binaries for all supported OSes"
|
||||
@@ -26,10 +25,11 @@ usage:
|
||||
@echo ""
|
||||
|
||||
test:
|
||||
go test -cover ./pkg/...
|
||||
go test -race -cover ./pkg/...
|
||||
|
||||
test-all:
|
||||
@./script/test_all.sh
|
||||
@./script/test_cockroach.sh
|
||||
|
||||
assets: static/
|
||||
go-bindata -o pkg/data/bindata.go -pkg data $(BINDATA_OPTS) $(BINDATA_IGNORE) -ignore=[.]gitignore -ignore=[.]gitkeep $<...
|
||||
@@ -49,12 +49,12 @@ release: clean assets
|
||||
@echo "Building binaries..."
|
||||
@gox \
|
||||
-osarch "$(TARGETS)" \
|
||||
-ldflags "-X github.com/sosedoff/pgweb/pkg/command.GitCommit $(GIT_COMMIT) -X github.com/sosedoff/pgweb/pkg/command.BuildTime $(BUILD_TIME)" \
|
||||
-ldflags "-X github.com/sosedoff/pgweb/pkg/command.GitCommit=$(GIT_COMMIT) -X github.com/sosedoff/pgweb/pkg/command.BuildTime=$(BUILD_TIME)" \
|
||||
-output "./bin/pgweb_{{.OS}}_{{.Arch}}"
|
||||
|
||||
@echo "Building ARM binaries..."
|
||||
GOOS=linux GOARCH=arm GOARM=5 go build \
|
||||
-ldflags "-X github.com/sosedoff/pgweb/pkg/command.GitCommit $(GIT_COMMIT) -X github.com/sosedoff/pgweb/pkg/command.BuildTime $(BUILD_TIME)" \
|
||||
-ldflags "-X github.com/sosedoff/pgweb/pkg/command.GitCommit=$(GIT_COMMIT) -X github.com/sosedoff/pgweb/pkg/command.BuildTime=$(BUILD_TIME)" \
|
||||
-o "./bin/pgweb_linux_arm_v5"
|
||||
|
||||
@echo "\nPackaging binaries...\n"
|
||||
@@ -64,11 +64,11 @@ bootstrap:
|
||||
gox -build-toolchain
|
||||
|
||||
setup:
|
||||
go get github.com/tools/godep
|
||||
go get golang.org/x/tools/cmd/cover
|
||||
go get github.com/mitchellh/gox
|
||||
go get github.com/jteeuwen/go-bindata/...
|
||||
godep restore
|
||||
go get -u github.com/golang/dep/cmd/dep
|
||||
go get -u golang.org/x/tools/cmd/cover
|
||||
go get -u github.com/mitchellh/gox
|
||||
go get -u github.com/go-bindata/go-bindata/...
|
||||
dep ensure
|
||||
|
||||
clean:
|
||||
@rm -f ./pgweb
|
||||
@@ -84,4 +84,4 @@ docker-release:
|
||||
|
||||
docker-push:
|
||||
docker push $(DOCKER_RELEASE_TAG)
|
||||
docker push $(DOCKER_LATEST_TAG)
|
||||
docker push $(DOCKER_LATEST_TAG)
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
Web-based PostgreSQL database browser written in Go.
|
||||
|
||||
[](https://github.com/sosedoff/pgweb/releases)
|
||||
[](https://travis-ci.org/sosedoff/pgweb)
|
||||
[](https://travis-ci.org/sosedoff/pgweb)
|
||||
[](https://ci.appveyor.com/project/sosedoff/pgweb)
|
||||
[](https://goreportcard.com/report/github.com/sosedoff/pgweb)
|
||||
[](https://godoc.org/github.com/sosedoff/pgweb)
|
||||
[](https://hub.docker.com/r/sosedoff/pgweb/)
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -32,6 +35,18 @@ application to work with local or remote PostgreSQL databases.
|
||||
|
||||
Visit [WIKI](https://github.com/sosedoff/pgweb/wiki) for more details
|
||||
|
||||
## Pgweb Pro
|
||||
|
||||
Pgweb Pro is the next major version of Pgweb and includes features like:
|
||||
|
||||
- Table structure editing
|
||||
- Data editing (update row content)
|
||||
- Charting
|
||||
- History persistence
|
||||
- Multiple tabs
|
||||
|
||||
Please get in touch via: https://goo.gl/forms/euQOGWg5uPdPH70b2
|
||||
|
||||
## Demo
|
||||
|
||||
Visit https://pgweb-demo.herokuapp.com to see pgweb in action.
|
||||
@@ -118,6 +133,6 @@ make test-all
|
||||
|
||||
## License
|
||||
|
||||
The MIT License (MIT)
|
||||
The MIT License (MIT). See [LICENSE](LICENSE) file for more details.
|
||||
|
||||
Copyright (c) 2014-2016 Dan Sosedoff, <dan.sosedoff@gmail.com>
|
||||
Copyright (c) 2014-2019 Dan Sosedoff, <dan.sosedoff@gmail.com>
|
||||
+6
-7
@@ -1,7 +1,8 @@
|
||||
clone_folder: c:\go\src\github.com\sosedoff\pgweb
|
||||
clone_folder: c:\gopath\src\github.com\sosedoff\pgweb
|
||||
|
||||
environment:
|
||||
GOPATH: c:\gopath
|
||||
CGO_ENABLED: 0
|
||||
|
||||
services:
|
||||
- postgresql
|
||||
@@ -13,15 +14,13 @@ install:
|
||||
- cd %APPVEYOR_BUILD_FOLDER%
|
||||
- go env
|
||||
- go version
|
||||
- go get github.com/tools/godep
|
||||
- godep get github.com/mitchellh/gox
|
||||
- godep get github.com/jteeuwen/go-bindata/...
|
||||
- godep restore
|
||||
- go get github.com/golang/dep/cmd/dep
|
||||
- dep ensure
|
||||
|
||||
build_script:
|
||||
- godep go build
|
||||
- go build
|
||||
|
||||
test_script:
|
||||
- godep go test -cover ./...
|
||||
- go test -cover ./...
|
||||
|
||||
deploy: off
|
||||
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=pgweb - Cross-platform client for PostgreSQL databases
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/pgweb --bind=0.0.0.0 --listen=8081
|
||||
Restart=on-abort
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/bin/sh
|
||||
### BEGIN INIT INFO
|
||||
# Provides: pgweb
|
||||
# Required-Start: $remote_fs $syslog
|
||||
# Required-Stop: $remote_fs $syslog
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: Start daemon at boot time
|
||||
# Description: Enable service provided by daemon.
|
||||
### END INIT INFO
|
||||
|
||||
# Installation instructions (originally written for Debian)
|
||||
#
|
||||
# Save this script into /etc/init.d/pgweb file, make it executable,
|
||||
# and install it into the boot sequence:
|
||||
#
|
||||
# chmod 755 /etc/init.d/pgweb
|
||||
# update-rc.d pgweb defaults
|
||||
#
|
||||
# This script assumes that pgweb binary is located at /home/pgweb/, and that
|
||||
# there's a bookmark 'server' in /home/pgweb/.pgweb/bookmarks/.
|
||||
#
|
||||
|
||||
NAME="pgweb"
|
||||
PIDFILE="/var/run/$NAME.pid"
|
||||
|
||||
USER="pgweb" # Linux system user
|
||||
SU="su $USER -s /bin/bash"
|
||||
|
||||
TIMEOUT=5 # Time in seconds to wait postgresql to show up
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
if [ -f $PIDFILE ]; then
|
||||
echo "Already running... cat $PIDFILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Wait postgresql to show up
|
||||
while ! test -f /var/run/postgresql/*main.pid
|
||||
do
|
||||
sleep 1
|
||||
TIMEOUT=`expr $TIMEOUT - 1`
|
||||
if test $TIMEOUT -eq 0; then
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Ready to start pgweb
|
||||
PID=`$SU -c '/home/pgweb/pgweb -s -b server >/dev/null & echo $!'` # Note! Logs are lost.
|
||||
if [ -z $PID ]; then
|
||||
exit 1
|
||||
else
|
||||
echo $PID > $PIDFILE
|
||||
fi
|
||||
;;
|
||||
stop)
|
||||
PID=`cat $PIDFILE`
|
||||
kill $PID && rm $PIDFILE
|
||||
;;
|
||||
*)
|
||||
echo "Usage: /etc/init.d/$NAME {start|stop}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,10 @@
|
||||
description "PgWeb as a Service for Ubuntu 14.04 With Upstart"
|
||||
|
||||
start on runlevel [2345]
|
||||
stop on runlevel [!2345]
|
||||
|
||||
respawn
|
||||
setuid youruser
|
||||
setgid www-data
|
||||
|
||||
exec /usr/bin/pgweb --bind=0.0.0.0 --listen=8081
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
host = "localhost"
|
||||
port = "5432"
|
||||
port = 5432
|
||||
user = "postgres"
|
||||
database = "mydatabase"
|
||||
ssl = "disable"
|
||||
|
||||
@@ -2,4 +2,4 @@ host = "localhost"
|
||||
port = 5432
|
||||
user = "postgres"
|
||||
database = "mydatabase"
|
||||
ssl = "disable"
|
||||
ssl = "disabled"
|
||||
@@ -17,6 +17,15 @@ CREATE DATABASE "booktown";
|
||||
-- Name: DATABASE "booktown" Type: COMMENT Owner:
|
||||
--
|
||||
|
||||
CREATE TABLE "dummies" (
|
||||
"id" integer NOT NULL,
|
||||
"isDummy" boolean
|
||||
);
|
||||
|
||||
INSERT INTO "dummies" VALUES (1, true);
|
||||
INSERT INTO "dummies" VALUES (2, true);
|
||||
|
||||
|
||||
COMMENT ON DATABASE "booktown" IS 'The Book Town Database.';
|
||||
|
||||
--
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
DROP DATABASE IF EXISTS "roach";
|
||||
CREATE DATABASE "roach";
|
||||
USE "roach";
|
||||
|
||||
CREATE TABLE product_information (
|
||||
product_id INT PRIMARY KEY NOT NULL,
|
||||
product_name STRING(50) UNIQUE NOT NULL,
|
||||
product_description STRING(2000),
|
||||
category_id STRING(1) NOT NULL CHECK (category_id IN ('A','B','C')),
|
||||
weight_class INT,
|
||||
warranty_period INT CONSTRAINT valid_warranty CHECK (warranty_period BETWEEN 0 AND 24),
|
||||
supplier_id INT,
|
||||
product_status STRING(20),
|
||||
list_price DECIMAL(8,2),
|
||||
min_price DECIMAL(8,2),
|
||||
catalog_url STRING(50) UNIQUE,
|
||||
date_added DATE DEFAULT CURRENT_DATE(),
|
||||
misc JSONB,
|
||||
CONSTRAINT price_check CHECK (list_price >= min_price),
|
||||
INDEX date_added_idx (date_added),
|
||||
INDEX supp_id_prod_status_idx (supplier_id, product_status),
|
||||
INVERTED INDEX details (misc)
|
||||
);
|
||||
|
||||
INSERT INTO product_information VALUES
|
||||
(1, 'Product A', 'Text', 'A', NULL, 1),
|
||||
(2, 'Product B', 'Text', 'B', NULL, 2),
|
||||
(3, 'Product C', 'Text', 'C', NULL, 3);
|
||||
|
||||
CREATE TABLE customers (
|
||||
id INT PRIMARY KEY,
|
||||
name STRING
|
||||
);
|
||||
|
||||
CREATE TABLE orders (
|
||||
id INT PRIMARY KEY,
|
||||
customer_id INT REFERENCES customers(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO customers VALUES (1, 'Lauren');
|
||||
INSERT INTO orders VALUES (1,1);
|
||||
DELETE FROM customers WHERE id = 1;
|
||||
SELECT * FROM orders;
|
||||
@@ -1,142 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/sosedoff/pgweb/pkg/api"
|
||||
"github.com/sosedoff/pgweb/pkg/client"
|
||||
"github.com/sosedoff/pgweb/pkg/command"
|
||||
"github.com/sosedoff/pgweb/pkg/connection"
|
||||
"github.com/sosedoff/pgweb/pkg/util"
|
||||
"github.com/sosedoff/pgweb/pkg/cli"
|
||||
)
|
||||
|
||||
var options command.Options
|
||||
|
||||
func exitWithMessage(message string) {
|
||||
fmt.Println("Error:", message)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func initClient() {
|
||||
if connection.IsBlank(command.Opts) {
|
||||
return
|
||||
}
|
||||
|
||||
cl, err := client.New()
|
||||
if err != nil {
|
||||
exitWithMessage(err.Error())
|
||||
}
|
||||
|
||||
if command.Opts.Debug {
|
||||
fmt.Println("Server connection string:", cl.ConnectionString)
|
||||
}
|
||||
|
||||
fmt.Println("Connecting to server...")
|
||||
err = cl.Test()
|
||||
if err != nil {
|
||||
exitWithMessage(err.Error())
|
||||
}
|
||||
|
||||
fmt.Println("Checking database objects...")
|
||||
_, err = cl.Objects()
|
||||
if err != nil {
|
||||
exitWithMessage(err.Error())
|
||||
}
|
||||
|
||||
api.DbClient = cl
|
||||
}
|
||||
|
||||
func initOptions() {
|
||||
err := command.ParseOptions()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
options = command.Opts
|
||||
|
||||
if options.Version {
|
||||
printVersion()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
printVersion()
|
||||
}
|
||||
|
||||
func printVersion() {
|
||||
str := fmt.Sprintf("Pgweb v%s", command.VERSION)
|
||||
if command.GitCommit != "" {
|
||||
str += fmt.Sprintf(" (git: %s)", command.GitCommit)
|
||||
}
|
||||
|
||||
fmt.Println(str)
|
||||
}
|
||||
|
||||
func startServer() {
|
||||
router := gin.Default()
|
||||
|
||||
// Enable HTTP basic authentication only if both user and password are set
|
||||
if options.AuthUser != "" && options.AuthPass != "" {
|
||||
auth := map[string]string{options.AuthUser: options.AuthPass}
|
||||
router.Use(gin.BasicAuth(auth))
|
||||
}
|
||||
|
||||
api.SetupRoutes(router)
|
||||
|
||||
fmt.Println("Starting server...")
|
||||
go func() {
|
||||
err := router.Run(fmt.Sprintf("%v:%v", options.HttpHost, options.HttpPort))
|
||||
if err != nil {
|
||||
fmt.Println("Cant start server:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func handleSignals() {
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt, os.Kill)
|
||||
<-c
|
||||
}
|
||||
|
||||
func openPage() {
|
||||
url := fmt.Sprintf("http://%v:%v/%s", options.HttpHost, options.HttpPort, options.Prefix)
|
||||
fmt.Println("To view database open", url, "in browser")
|
||||
|
||||
if options.SkipOpen {
|
||||
return
|
||||
}
|
||||
|
||||
_, err := exec.Command("which", "open").Output()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
exec.Command("open", url).Output()
|
||||
}
|
||||
|
||||
func main() {
|
||||
initOptions()
|
||||
initClient()
|
||||
|
||||
if api.DbClient != nil {
|
||||
defer api.DbClient.Close()
|
||||
}
|
||||
|
||||
if !options.Debug {
|
||||
gin.SetMode("release")
|
||||
}
|
||||
|
||||
// Print memory usage every 30 seconds with debug flag
|
||||
if options.Debug {
|
||||
util.StartProfiler()
|
||||
}
|
||||
|
||||
startServer()
|
||||
openPage()
|
||||
handleSignals()
|
||||
cli.Run()
|
||||
}
|
||||
|
||||
+214
-50
@@ -4,9 +4,12 @@ import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
neturl "net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/tuvistavie/securerandom"
|
||||
|
||||
"github.com/sosedoff/pgweb/pkg/bookmarks"
|
||||
"github.com/sosedoff/pgweb/pkg/client"
|
||||
@@ -16,10 +19,14 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
DbClient *client.Client
|
||||
// DbClient represents the active database connection in a single-session mode
|
||||
DbClient *client.Client
|
||||
|
||||
// DbSessions represents the mapping for client connections
|
||||
DbSessions = map[string]*client.Client{}
|
||||
)
|
||||
|
||||
// DB returns a database connection from the client context
|
||||
func DB(c *gin.Context) *client.Client {
|
||||
if command.Opts.Sessions {
|
||||
return DbSessions[getSessionId(c.Request)]
|
||||
@@ -60,27 +67,76 @@ func GetSessions(c *gin.Context) {
|
||||
// In debug mode endpoint will return a lot of sensitive information
|
||||
// like full database connection string and all query history.
|
||||
if command.Opts.Debug {
|
||||
c.JSON(200, DbSessions)
|
||||
successResponse(c, DbSessions)
|
||||
return
|
||||
}
|
||||
successResponse(c, gin.H{"sessions": len(DbSessions)})
|
||||
}
|
||||
|
||||
func ConnectWithBackend(c *gin.Context) {
|
||||
// Setup a new backend client
|
||||
backend := Backend{
|
||||
Endpoint: command.Opts.ConnectBackend,
|
||||
Token: command.Opts.ConnectToken,
|
||||
PassHeaders: command.Opts.ConnectHeaders,
|
||||
}
|
||||
|
||||
// Fetch connection credentials
|
||||
cred, err := backend.FetchCredential(c.Param("resource"), c)
|
||||
if err != nil {
|
||||
badRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, map[string]int{"sessions": len(DbSessions)})
|
||||
// Make the new session
|
||||
sessionId, err := securerandom.Uuid()
|
||||
if err != nil {
|
||||
badRequest(c, err)
|
||||
return
|
||||
}
|
||||
c.Request.Header.Add("x-session-id", sessionId)
|
||||
|
||||
// Connect to the database
|
||||
cl, err := client.NewFromUrl(cred.DatabaseURL, nil)
|
||||
if err != nil {
|
||||
badRequest(c, err)
|
||||
return
|
||||
}
|
||||
cl.External = true
|
||||
|
||||
// Finalize session seetup
|
||||
_, err = cl.Info()
|
||||
if err == nil {
|
||||
err = setClient(c, cl)
|
||||
}
|
||||
if err != nil {
|
||||
cl.Close()
|
||||
badRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(301, fmt.Sprintf("/%s?session=%s", command.Opts.Prefix, sessionId))
|
||||
}
|
||||
|
||||
func Connect(c *gin.Context) {
|
||||
if command.Opts.LockSession {
|
||||
badRequest(c, "Session is locked")
|
||||
return
|
||||
}
|
||||
|
||||
var sshInfo *shared.SSHInfo
|
||||
url := c.Request.FormValue("url")
|
||||
|
||||
if url == "" {
|
||||
c.JSON(400, Error{"Url parameter is required"})
|
||||
badRequest(c, "Url parameter is required")
|
||||
return
|
||||
}
|
||||
|
||||
opts := command.Options{Url: url}
|
||||
url, err := connection.FormatUrl(opts)
|
||||
url, err := connection.FormatURL(opts)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, Error{err.Error()})
|
||||
badRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -90,67 +146,138 @@ func Connect(c *gin.Context) {
|
||||
|
||||
cl, err := client.NewFromUrl(url, sshInfo)
|
||||
if err != nil {
|
||||
c.JSON(400, Error{err.Error()})
|
||||
badRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = cl.Test()
|
||||
if err != nil {
|
||||
c.JSON(400, Error{err.Error()})
|
||||
badRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
info, err := cl.Info()
|
||||
if err == nil {
|
||||
err = setClient(c, cl)
|
||||
if err != nil {
|
||||
cl.Close()
|
||||
c.JSON(400, Error{err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
cl.Close()
|
||||
badRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, info.Format()[0])
|
||||
successResponse(c, info.Format()[0])
|
||||
}
|
||||
|
||||
func SwitchDb(c *gin.Context) {
|
||||
if command.Opts.LockSession {
|
||||
badRequest(c, "Session is locked")
|
||||
return
|
||||
}
|
||||
|
||||
name := c.Request.URL.Query().Get("db")
|
||||
if name == "" {
|
||||
name = c.Request.FormValue("db")
|
||||
}
|
||||
if name == "" {
|
||||
badRequest(c, "Database name is not provided")
|
||||
return
|
||||
}
|
||||
|
||||
conn := DB(c)
|
||||
if conn == nil {
|
||||
badRequest(c, "Not connected")
|
||||
return
|
||||
}
|
||||
|
||||
// Do not allow switching databases for connections from third-party backends
|
||||
if conn.External {
|
||||
badRequest(c, "Session is locked")
|
||||
return
|
||||
}
|
||||
|
||||
currentUrl, err := neturl.Parse(conn.ConnectionString)
|
||||
if err != nil {
|
||||
badRequest(c, "Unable to parse current connection string")
|
||||
return
|
||||
}
|
||||
|
||||
currentUrl.Path = name
|
||||
|
||||
cl, err := client.NewFromUrl(currentUrl.String(), nil)
|
||||
if err != nil {
|
||||
badRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = cl.Test()
|
||||
if err != nil {
|
||||
badRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
info, err := cl.Info()
|
||||
if err == nil {
|
||||
err = setClient(c, cl)
|
||||
}
|
||||
if err != nil {
|
||||
cl.Close()
|
||||
badRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
conn.Close()
|
||||
|
||||
successResponse(c, info.Format()[0])
|
||||
}
|
||||
|
||||
func Disconnect(c *gin.Context) {
|
||||
if command.Opts.LockSession {
|
||||
badRequest(c, "Session is locked")
|
||||
return
|
||||
}
|
||||
|
||||
conn := DB(c)
|
||||
|
||||
if conn == nil {
|
||||
c.JSON(400, Error{"Not connected"})
|
||||
badRequest(c, "Not connected")
|
||||
return
|
||||
}
|
||||
|
||||
err := conn.Close()
|
||||
if err != nil {
|
||||
c.JSON(400, Error{err.Error()})
|
||||
badRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, map[string]bool{"success": true})
|
||||
successResponse(c, gin.H{"success": true})
|
||||
}
|
||||
|
||||
func GetDatabases(c *gin.Context) {
|
||||
conn := DB(c)
|
||||
if conn.External {
|
||||
errorResponse(c, 403, "Not permitted")
|
||||
return
|
||||
}
|
||||
|
||||
names, err := DB(c).Databases()
|
||||
serveResult(names, err, c)
|
||||
serveResult(c, names, err)
|
||||
}
|
||||
|
||||
func GetObjects(c *gin.Context) {
|
||||
result, err := DB(c).Objects()
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
badRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
objects := client.ObjectsFromResult(result)
|
||||
c.JSON(200, objects)
|
||||
successResponse(c, client.ObjectsFromResult(result))
|
||||
}
|
||||
|
||||
func RunQuery(c *gin.Context) {
|
||||
query := cleanQuery(c.Request.FormValue("query"))
|
||||
|
||||
if query == "" {
|
||||
c.JSON(400, NewError(errors.New("Query parameter is missing")))
|
||||
badRequest(c, "Query parameter is missing")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -161,7 +288,7 @@ func ExplainQuery(c *gin.Context) {
|
||||
query := cleanQuery(c.Request.FormValue("query"))
|
||||
|
||||
if query == "" {
|
||||
c.JSON(400, NewError(errors.New("Query parameter is missing")))
|
||||
badRequest(c, "Query parameter is missing")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -170,7 +297,7 @@ func ExplainQuery(c *gin.Context) {
|
||||
|
||||
func GetSchemas(c *gin.Context) {
|
||||
res, err := DB(c).Schemas()
|
||||
serveResult(res, err, c)
|
||||
serveResult(c, res, err)
|
||||
}
|
||||
|
||||
func GetTable(c *gin.Context) {
|
||||
@@ -183,19 +310,19 @@ func GetTable(c *gin.Context) {
|
||||
res, err = DB(c).Table(c.Params.ByName("table"))
|
||||
}
|
||||
|
||||
serveResult(res, err, c)
|
||||
serveResult(c, res, err)
|
||||
}
|
||||
|
||||
func GetTableRows(c *gin.Context) {
|
||||
offset, err := parseIntFormValue(c, "offset", 0)
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
badRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
limit, err := parseIntFormValue(c, "limit", 100)
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
badRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -209,13 +336,13 @@ func GetTableRows(c *gin.Context) {
|
||||
|
||||
res, err := DB(c).TableRows(c.Params.ByName("table"), opts)
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
badRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
countRes, err := DB(c).TableRowsCount(c.Params.ByName("table"), opts)
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
badRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -235,48 +362,49 @@ func GetTableRows(c *gin.Context) {
|
||||
PerPage: numFetch,
|
||||
}
|
||||
|
||||
serveResult(res, err, c)
|
||||
serveResult(c, res, err)
|
||||
}
|
||||
|
||||
func GetTableInfo(c *gin.Context) {
|
||||
res, err := DB(c).TableInfo(c.Params.ByName("table"))
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
if err == nil {
|
||||
successResponse(c, res.Format()[0])
|
||||
} else {
|
||||
badRequest(c, err)
|
||||
}
|
||||
|
||||
c.JSON(200, res.Format()[0])
|
||||
}
|
||||
|
||||
func GetHistory(c *gin.Context) {
|
||||
c.JSON(200, DB(c).History)
|
||||
successResponse(c, DB(c).History)
|
||||
}
|
||||
|
||||
func GetConnectionInfo(c *gin.Context) {
|
||||
res, err := DB(c).Info()
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
badRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, res.Format()[0])
|
||||
info := res.Format()[0]
|
||||
info["session_lock"] = command.Opts.LockSession
|
||||
|
||||
successResponse(c, info)
|
||||
}
|
||||
|
||||
func GetActivity(c *gin.Context) {
|
||||
res, err := DB(c).Activity()
|
||||
serveResult(res, err, c)
|
||||
serveResult(c, res, err)
|
||||
}
|
||||
|
||||
func GetTableIndexes(c *gin.Context) {
|
||||
res, err := DB(c).TableIndexes(c.Params.ByName("table"))
|
||||
serveResult(res, err, c)
|
||||
serveResult(c, res, err)
|
||||
}
|
||||
|
||||
func GetTableConstraints(c *gin.Context) {
|
||||
res, err := DB(c).TableConstraints(c.Params.ByName("table"))
|
||||
serveResult(res, err, c)
|
||||
serveResult(c, res, err)
|
||||
}
|
||||
|
||||
func HandleQuery(query string, c *gin.Context) {
|
||||
@@ -287,7 +415,7 @@ func HandleQuery(query string, c *gin.Context) {
|
||||
|
||||
result, err := DB(c).Query(query)
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
badRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -306,7 +434,7 @@ func HandleQuery(query string, c *gin.Context) {
|
||||
case "csv":
|
||||
c.Data(200, "text/csv", result.CSV())
|
||||
case "json":
|
||||
c.Data(200, "applicaiton/json", result.JSON())
|
||||
c.Data(200, "application/json", result.JSON())
|
||||
case "xml":
|
||||
c.XML(200, result)
|
||||
default:
|
||||
@@ -315,16 +443,52 @@ func HandleQuery(query string, c *gin.Context) {
|
||||
}
|
||||
|
||||
func GetBookmarks(c *gin.Context) {
|
||||
bookmarks, err := bookmarks.ReadAll(bookmarks.Path())
|
||||
serveResult(bookmarks, err, c)
|
||||
bookmarks, err := bookmarks.ReadAll(bookmarks.Path(command.Opts.BookmarksDir))
|
||||
serveResult(c, bookmarks, err)
|
||||
}
|
||||
|
||||
func GetInfo(c *gin.Context) {
|
||||
info := map[string]string{
|
||||
"version": command.VERSION,
|
||||
successResponse(c, gin.H{
|
||||
"version": command.Version,
|
||||
"git_sha": command.GitCommit,
|
||||
"build_time": command.BuildTime,
|
||||
})
|
||||
}
|
||||
|
||||
// Export database or table data
|
||||
func DataExport(c *gin.Context) {
|
||||
db := DB(c)
|
||||
|
||||
info, err := db.Info()
|
||||
if err != nil {
|
||||
badRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, info)
|
||||
dump := client.Dump{
|
||||
Table: strings.TrimSpace(c.Request.FormValue("table")),
|
||||
}
|
||||
|
||||
// If pg_dump is not available the following code will not show an error in browser.
|
||||
// This is due to the headers being written first.
|
||||
if !dump.CanExport() {
|
||||
badRequest(c, "pg_dump is not found")
|
||||
return
|
||||
}
|
||||
|
||||
formattedInfo := info.Format()[0]
|
||||
filename := formattedInfo["current_database"].(string)
|
||||
if dump.Table != "" {
|
||||
filename = filename + "_" + dump.Table
|
||||
}
|
||||
|
||||
c.Header(
|
||||
"Content-Disposition",
|
||||
fmt.Sprintf(`attachment; filename="%s.sql.gz"`, filename),
|
||||
)
|
||||
|
||||
err = dump.Export(db.ConnectionString, c.Writer)
|
||||
if err != nil {
|
||||
badRequest(c, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,6 @@ func Test_assetContentType(t *testing.T) {
|
||||
"foo.gif": "image/gif",
|
||||
"foo.eot": "application/vnd.ms-fontobject",
|
||||
"foo.svg": "image/svg+xml",
|
||||
"foo.ttf": "application/x-font-ttf",
|
||||
"foo.woff": "application/x-font-woff",
|
||||
"foo.foo": "text/plain; charset=utf-8",
|
||||
"foo": "text/plain; charset=utf-8",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Backend represents a third party configuration source
|
||||
type Backend struct {
|
||||
Endpoint string
|
||||
Token string
|
||||
PassHeaders string
|
||||
}
|
||||
|
||||
// BackendRequest represents a payload sent to the third-party source
|
||||
type BackendRequest struct {
|
||||
Resource string `json:"resource"`
|
||||
Token string `json:"token"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
}
|
||||
|
||||
// BackendCredential represents the third-party response
|
||||
type BackendCredential struct {
|
||||
DatabaseURL string `json:"database_url"`
|
||||
}
|
||||
|
||||
// FetchCredential sends an authentication request to a third-party service
|
||||
func (be Backend) FetchCredential(resource string, c *gin.Context) (*BackendCredential, error) {
|
||||
request := BackendRequest{
|
||||
Resource: resource,
|
||||
Token: be.Token,
|
||||
Headers: map[string]string{},
|
||||
}
|
||||
|
||||
// Pass white-listed client headers to the backend request
|
||||
for _, name := range strings.Split(be.PassHeaders, ",") {
|
||||
request.Headers[strings.ToLower(name)] = c.Request.Header.Get(name)
|
||||
}
|
||||
|
||||
body, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := http.Post(be.Endpoint, "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
// Any connection-related issues will show up in the server log
|
||||
log.Println("Unable to fetch backend credential:", err)
|
||||
|
||||
// We dont want to expose the url of the backend here, so reply with generic error
|
||||
return nil, fmt.Errorf("Unable to connect to the auth backend")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("Got HTTP error %v from backend", resp.StatusCode)
|
||||
}
|
||||
|
||||
cred := &BackendCredential{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(cred); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cred.DatabaseURL == "" {
|
||||
return nil, fmt.Errorf("Database URL was not provided")
|
||||
}
|
||||
|
||||
return cred, nil
|
||||
}
|
||||
+72
-22
@@ -10,33 +10,37 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/sosedoff/pgweb/pkg/data"
|
||||
"github.com/sosedoff/pgweb/pkg/shared"
|
||||
)
|
||||
|
||||
var extraMimeTypes = map[string]string{
|
||||
".icon": "image-x-icon",
|
||||
".ttf": "application/x-font-ttf",
|
||||
".woff": "application/x-font-woff",
|
||||
".eot": "application/vnd.ms-fontobject",
|
||||
".svg": "image/svg+xml",
|
||||
".html": "text/html; charset-utf-8",
|
||||
}
|
||||
var (
|
||||
// Mime types definitions
|
||||
extraMimeTypes = map[string]string{
|
||||
".icon": "image-x-icon",
|
||||
".ttf": "application/x-font-ttf",
|
||||
".woff": "application/x-font-woff",
|
||||
".eot": "application/vnd.ms-fontobject",
|
||||
".svg": "image/svg+xml",
|
||||
".html": "text/html; charset-utf-8",
|
||||
}
|
||||
|
||||
// Paths that dont require database connection
|
||||
var allowedPaths = map[string]bool{
|
||||
"/api/sessions": true,
|
||||
"/api/info": true,
|
||||
"/api/connect": true,
|
||||
"/api/bookmarks": true,
|
||||
"/api/history": true,
|
||||
}
|
||||
// Paths that dont require database connection
|
||||
allowedPaths = map[string]bool{
|
||||
"/api/sessions": true,
|
||||
"/api/info": true,
|
||||
"/api/connect": true,
|
||||
"/api/bookmarks": true,
|
||||
"/api/history": true,
|
||||
}
|
||||
|
||||
// List of characters replaced by javascript code to make queries url-safe.
|
||||
var base64subs = map[string]string{
|
||||
"-": "+",
|
||||
"_": "/",
|
||||
".": "=",
|
||||
}
|
||||
// List of characters replaced by javascript code to make queries url-safe.
|
||||
base64subs = map[string]string{
|
||||
"-": "+",
|
||||
"_": "/",
|
||||
".": "=",
|
||||
}
|
||||
)
|
||||
|
||||
type Error struct {
|
||||
Message string `json:"error"`
|
||||
@@ -115,6 +119,7 @@ func parseSshInfo(c *gin.Context) *shared.SSHInfo {
|
||||
Port: c.Request.FormValue("ssh_port"),
|
||||
User: c.Request.FormValue("ssh_user"),
|
||||
Password: c.Request.FormValue("ssh_password"),
|
||||
Key: c.Request.FormValue("ssh_key"),
|
||||
}
|
||||
|
||||
if info.Port == "" {
|
||||
@@ -138,3 +143,48 @@ func assetContentType(name string) string {
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func serveStaticAsset(path string, c *gin.Context) {
|
||||
data, err := data.Asset("static" + path)
|
||||
if err != nil {
|
||||
c.String(400, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data(200, assetContentType(path), data)
|
||||
}
|
||||
|
||||
// Send a query result to client
|
||||
func serveResult(c *gin.Context, result interface{}, err interface{}) {
|
||||
if err == nil {
|
||||
successResponse(c, result)
|
||||
} else {
|
||||
badRequest(c, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Send successful response back to client
|
||||
func successResponse(c *gin.Context, data interface{}) {
|
||||
c.JSON(200, data)
|
||||
}
|
||||
|
||||
// Send an error response back to client
|
||||
func errorResponse(c *gin.Context, status int, err interface{}) {
|
||||
var message interface{}
|
||||
|
||||
switch v := err.(type) {
|
||||
case error:
|
||||
message = v.Error()
|
||||
case string:
|
||||
message = v
|
||||
default:
|
||||
message = v
|
||||
}
|
||||
|
||||
c.AbortWithStatusJSON(status, gin.H{"status": status, "error": message})
|
||||
}
|
||||
|
||||
// Send a bad request (http 400) back to client
|
||||
func badRequest(c *gin.Context, err interface{}) {
|
||||
errorResponse(c, 400, err)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -36,3 +39,34 @@ func Test_getSessionId(t *testing.T) {
|
||||
req.URL, _ = url.Parse("http://foobar/?_session_id=token")
|
||||
assert.Equal(t, "token", getSessionId(req))
|
||||
}
|
||||
|
||||
func Test_serveResult(t *testing.T) {
|
||||
server := gin.Default()
|
||||
server.GET("/good", func(c *gin.Context) {
|
||||
serveResult(c, gin.H{"foo": "bar"}, nil)
|
||||
})
|
||||
server.GET("/bad", func(c *gin.Context) {
|
||||
serveResult(c, nil, errors.New("message"))
|
||||
})
|
||||
server.GET("/nodata", func(c *gin.Context) {
|
||||
serveResult(c, nil, nil)
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/good", nil)
|
||||
server.ServeHTTP(w, req)
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.Equal(t, `{"foo":"bar"}`, w.Body.String())
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", "/bad", nil)
|
||||
server.ServeHTTP(w, req)
|
||||
assert.Equal(t, 400, w.Code)
|
||||
assert.Equal(t, `{"error":"message","status":400}`, w.Body.String())
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", "/nodata", nil)
|
||||
server.ServeHTTP(w, req)
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.Equal(t, `null`, w.Body.String())
|
||||
}
|
||||
|
||||
+16
-27
@@ -7,24 +7,23 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/sosedoff/pgweb/pkg/command"
|
||||
"github.com/sosedoff/pgweb/pkg/data"
|
||||
)
|
||||
|
||||
// Middleware function to check database connection status before running queries
|
||||
// Middleware to check database connection status before running queries
|
||||
func dbCheckMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
path := strings.Replace(c.Request.URL.Path, command.Opts.Prefix, "", -1)
|
||||
|
||||
if allowedPaths[path] == true {
|
||||
// Allow whitelisted paths
|
||||
if allowedPaths[path] {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// We dont care about sessions unless they're enabled
|
||||
// Check if session exists in single-session mode
|
||||
if !command.Opts.Sessions {
|
||||
if DbClient == nil {
|
||||
c.JSON(400, Error{"Not connected"})
|
||||
c.Abort()
|
||||
badRequest(c, "Not connected")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -32,17 +31,17 @@ func dbCheckMiddleware() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// Determine session ID from the client request
|
||||
sessionId := getSessionId(c.Request)
|
||||
if sessionId == "" {
|
||||
c.JSON(400, Error{"Session ID is required"})
|
||||
c.Abort()
|
||||
badRequest(c, "Session ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Determine the database connection handle for the session
|
||||
conn := DbSessions[sessionId]
|
||||
if conn == nil {
|
||||
c.JSON(400, Error{"Not connected"})
|
||||
c.Abort()
|
||||
badRequest(c, "Not connected")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -50,7 +49,7 @@ func dbCheckMiddleware() gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// Middleware function to print out request parameters and body for debugging
|
||||
// Middleware to print out request parameters and body for debugging
|
||||
func requestInspectMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
err := c.Request.ParseForm()
|
||||
@@ -58,21 +57,11 @@ func requestInspectMiddleware() gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func serveStaticAsset(path string, c *gin.Context) {
|
||||
data, err := data.Asset("static" + path)
|
||||
if err != nil {
|
||||
c.String(400, err.Error())
|
||||
return
|
||||
// Middleware to inject CORS headers
|
||||
func corsMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
c.Header("Access-Control-Expose-Headers", "*")
|
||||
c.Header("Access-Control-Allow-Origin", command.Opts.CorsOrigin)
|
||||
}
|
||||
|
||||
c.Data(200, assetContentType(path), data)
|
||||
}
|
||||
|
||||
func serveResult(result interface{}, err error, c *gin.Context) {
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, result)
|
||||
}
|
||||
|
||||
+34
-29
@@ -10,41 +10,46 @@ func SetupMiddlewares(group *gin.RouterGroup) {
|
||||
group.Use(requestInspectMiddleware())
|
||||
}
|
||||
|
||||
if command.Opts.Cors {
|
||||
group.Use(corsMiddleware())
|
||||
}
|
||||
|
||||
group.Use(dbCheckMiddleware())
|
||||
}
|
||||
|
||||
func SetupRoutes(router *gin.Engine) {
|
||||
group := router.Group(command.Opts.Prefix)
|
||||
root := router.Group(command.Opts.Prefix)
|
||||
|
||||
group.GET("/", GetHome)
|
||||
group.GET("/static/*path", GetAsset)
|
||||
root.GET("/", GetHome)
|
||||
root.GET("/static/*path", GetAsset)
|
||||
root.GET("/connect/:resource", ConnectWithBackend)
|
||||
|
||||
api := group.Group("/api")
|
||||
{
|
||||
SetupMiddlewares(api)
|
||||
api := root.Group("/api")
|
||||
SetupMiddlewares(api)
|
||||
|
||||
if command.Opts.Sessions {
|
||||
api.GET("/sessions", GetSessions)
|
||||
}
|
||||
|
||||
api.GET("/info", GetInfo)
|
||||
api.POST("/connect", Connect)
|
||||
api.POST("/disconnect", Disconnect)
|
||||
api.GET("/databases", GetDatabases)
|
||||
api.GET("/connection", GetConnectionInfo)
|
||||
api.GET("/activity", GetActivity)
|
||||
api.GET("/schemas", GetSchemas)
|
||||
api.GET("/objects", GetObjects)
|
||||
api.GET("/tables/:table", GetTable)
|
||||
api.GET("/tables/:table/rows", GetTableRows)
|
||||
api.GET("/tables/:table/info", GetTableInfo)
|
||||
api.GET("/tables/:table/indexes", GetTableIndexes)
|
||||
api.GET("/tables/:table/constraints", GetTableConstraints)
|
||||
api.GET("/query", RunQuery)
|
||||
api.POST("/query", RunQuery)
|
||||
api.GET("/explain", ExplainQuery)
|
||||
api.POST("/explain", ExplainQuery)
|
||||
api.GET("/history", GetHistory)
|
||||
api.GET("/bookmarks", GetBookmarks)
|
||||
if command.Opts.Sessions {
|
||||
api.GET("/sessions", GetSessions)
|
||||
}
|
||||
|
||||
api.GET("/info", GetInfo)
|
||||
api.POST("/connect", Connect)
|
||||
api.POST("/disconnect", Disconnect)
|
||||
api.POST("/switchdb", SwitchDb)
|
||||
api.GET("/databases", GetDatabases)
|
||||
api.GET("/connection", GetConnectionInfo)
|
||||
api.GET("/activity", GetActivity)
|
||||
api.GET("/schemas", GetSchemas)
|
||||
api.GET("/objects", GetObjects)
|
||||
api.GET("/tables/:table", GetTable)
|
||||
api.GET("/tables/:table/rows", GetTableRows)
|
||||
api.GET("/tables/:table/info", GetTableInfo)
|
||||
api.GET("/tables/:table/indexes", GetTableIndexes)
|
||||
api.GET("/tables/:table/constraints", GetTableConstraints)
|
||||
api.GET("/query", RunQuery)
|
||||
api.POST("/query", RunQuery)
|
||||
api.GET("/explain", ExplainQuery)
|
||||
api.POST("/explain", ExplainQuery)
|
||||
api.GET("/history", GetHistory)
|
||||
api.GET("/bookmarks", GetBookmarks)
|
||||
api.GET("/export", DataExport)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/sosedoff/pgweb/pkg/command"
|
||||
)
|
||||
|
||||
// StartSessionCleanup starts a goroutine to cleanup idle database sessions
|
||||
func StartSessionCleanup() {
|
||||
for range time.Tick(time.Minute) {
|
||||
if command.Opts.Debug {
|
||||
log.Println("Triggering idle session deletion")
|
||||
}
|
||||
cleanupIdleSessions()
|
||||
}
|
||||
}
|
||||
|
||||
func cleanupIdleSessions() {
|
||||
ids := []string{}
|
||||
|
||||
// Figure out which sessions are idle
|
||||
for id, client := range DbSessions {
|
||||
if client.IsIdle() {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Close and delete idle sessions
|
||||
log.Println("Closing", len(ids), "idle sessions")
|
||||
for _, id := range ids {
|
||||
// TODO: concurrent map edit will trigger panic
|
||||
if err := DbSessions[id].Close(); err == nil {
|
||||
delete(DbSessions, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
+72
-11
@@ -9,18 +9,35 @@ import (
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
|
||||
"github.com/sosedoff/pgweb/pkg/command"
|
||||
"github.com/sosedoff/pgweb/pkg/shared"
|
||||
)
|
||||
|
||||
type Bookmark struct {
|
||||
Url string `json:"url"` // Postgres connection URL
|
||||
Host string `json:"host"` // Server hostname
|
||||
Port string `json:"port"` // Server port
|
||||
User string `json:"user"` // Database user
|
||||
Password string `json:"password"` // User password
|
||||
Database string `json:"database"` // Database name
|
||||
Ssl string `json:"ssl"` // Connection SSL mode
|
||||
Ssh shared.SSHInfo `json:"ssh"` // SSH tunnel config
|
||||
Url string `json:"url"` // Postgres connection URL
|
||||
Host string `json:"host"` // Server hostname
|
||||
Port int `json:"port"` // Server port
|
||||
User string `json:"user"` // Database user
|
||||
Password string `json:"password"` // User password
|
||||
Database string `json:"database"` // Database name
|
||||
Ssl string `json:"ssl"` // Connection SSL mode
|
||||
Ssh *shared.SSHInfo `json:"ssh"` // SSH tunnel config
|
||||
}
|
||||
|
||||
func (b Bookmark) SSHInfoIsEmpty() bool {
|
||||
return b.Ssh == nil || b.Ssh.User == "" && b.Ssh.Host == "" && b.Ssh.Port == ""
|
||||
}
|
||||
|
||||
func (b Bookmark) ConvertToOptions() command.Options {
|
||||
return command.Options{
|
||||
Url: b.Url,
|
||||
Host: b.Host,
|
||||
Port: b.Port,
|
||||
User: b.User,
|
||||
Pass: b.Password,
|
||||
DbName: b.Database,
|
||||
Ssl: b.Ssl,
|
||||
}
|
||||
}
|
||||
|
||||
func readServerConfig(path string) (Bookmark, error) {
|
||||
@@ -32,6 +49,32 @@ func readServerConfig(path string) (Bookmark, error) {
|
||||
}
|
||||
|
||||
_, err = toml.Decode(string(buff), &bookmark)
|
||||
|
||||
if bookmark.Port == 0 {
|
||||
bookmark.Port = 5432
|
||||
}
|
||||
|
||||
// List of all supported postgres modes
|
||||
modes := []string{"disable", "allow", "prefer", "require", "verify-ca", "verify-full"}
|
||||
valid := false
|
||||
|
||||
for _, mode := range modes {
|
||||
if bookmark.Ssl == mode {
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to a default mode if mode is not set or invalid
|
||||
// Typical typo: ssl mode set to "disabled"
|
||||
if bookmark.Ssl == "" || !valid {
|
||||
bookmark.Ssl = "disable"
|
||||
}
|
||||
|
||||
if bookmark.Ssh != nil && bookmark.Ssh.Port == "" {
|
||||
bookmark.Ssh.Port = "22"
|
||||
}
|
||||
|
||||
return bookmark, err
|
||||
}
|
||||
|
||||
@@ -40,9 +83,13 @@ func fileBasename(path string) string {
|
||||
return strings.Replace(filename, filepath.Ext(path), "", 1)
|
||||
}
|
||||
|
||||
func Path() string {
|
||||
path, _ := homedir.Dir()
|
||||
return fmt.Sprintf("%s/.pgweb/bookmarks", path)
|
||||
func Path(overrideDir string) string {
|
||||
if overrideDir == "" {
|
||||
path, _ := homedir.Dir()
|
||||
return fmt.Sprintf("%s/.pgweb/bookmarks", path)
|
||||
}
|
||||
|
||||
return overrideDir
|
||||
}
|
||||
|
||||
func ReadAll(path string) (map[string]Bookmark, error) {
|
||||
@@ -72,3 +119,17 @@ func ReadAll(path string) (map[string]Bookmark, error) {
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func GetBookmark(bookmarkPath string, bookmarkName string) (Bookmark, error) {
|
||||
bookmarks, err := ReadAll(bookmarkPath)
|
||||
if err != nil {
|
||||
return Bookmark{}, err
|
||||
}
|
||||
|
||||
bookmark, ok := bookmarks[bookmarkName]
|
||||
if !ok {
|
||||
return Bookmark{}, fmt.Errorf("couldn't find a bookmark with name %s", bookmarkName)
|
||||
}
|
||||
|
||||
return bookmark, nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package bookmarks
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sosedoff/pgweb/pkg/command"
|
||||
"github.com/sosedoff/pgweb/pkg/shared"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -12,24 +14,23 @@ func Test_Invalid_Bookmark_Files(t *testing.T) {
|
||||
|
||||
_, err = readServerConfig("../../data/invalid.toml")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Near line 1, key 'invalid encoding': Near line 2: Expected key separator '=', but got '\\n' instead.", err.Error())
|
||||
|
||||
_, err = readServerConfig("../../data/invalid_port.toml")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Type mismatch for 'bookmarks.Bookmark.Port': Expected string but found 'int64'.", err.Error())
|
||||
assert.Equal(t, "Near line 1 (last key parsed 'invalid'): expected key separator '=', but got 'e' instead", err.Error())
|
||||
}
|
||||
|
||||
func Test_Bookmark(t *testing.T) {
|
||||
bookmark, err := readServerConfig("../../data/bookmark.toml")
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "localhost", bookmark.Host)
|
||||
assert.Equal(t, "5432", bookmark.Port)
|
||||
assert.Equal(t, 5432, bookmark.Port)
|
||||
assert.Equal(t, "postgres", bookmark.User)
|
||||
assert.Equal(t, "mydatabase", bookmark.Database)
|
||||
assert.Equal(t, "disable", bookmark.Ssl)
|
||||
assert.Equal(t, "", bookmark.Password)
|
||||
assert.Equal(t, "", bookmark.Url)
|
||||
|
||||
bookmark, err = readServerConfig("../../data/bookmark_invalid_ssl.toml")
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "disable", bookmark.Ssl)
|
||||
}
|
||||
|
||||
func Test_Bookmark_URL(t *testing.T) {
|
||||
@@ -38,15 +39,15 @@ func Test_Bookmark_URL(t *testing.T) {
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://username:password@host:port/database?sslmode=disable", bookmark.Url)
|
||||
assert.Equal(t, "", bookmark.Host)
|
||||
assert.Equal(t, "", bookmark.Port)
|
||||
assert.Equal(t, 5432, bookmark.Port)
|
||||
assert.Equal(t, "", bookmark.User)
|
||||
assert.Equal(t, "", bookmark.Database)
|
||||
assert.Equal(t, "", bookmark.Ssl)
|
||||
assert.Equal(t, "disable", bookmark.Ssl)
|
||||
assert.Equal(t, "", bookmark.Password)
|
||||
}
|
||||
|
||||
func Test_Bookmarks_Path(t *testing.T) {
|
||||
assert.NotEqual(t, "/.pgweb/bookmarks", Path())
|
||||
assert.NotEqual(t, "/.pgweb/bookmarks", Path(""))
|
||||
}
|
||||
|
||||
func Test_Basename(t *testing.T) {
|
||||
@@ -67,5 +68,74 @@ func Test_ReadBookmarks(t *testing.T) {
|
||||
bookmarks, err := ReadAll("../../data")
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, 2, len(bookmarks))
|
||||
assert.Equal(t, 3, len(bookmarks))
|
||||
}
|
||||
|
||||
func Test_GetBookmark(t *testing.T) {
|
||||
expBookmark := Bookmark{
|
||||
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
User: "postgres",
|
||||
Password: "",
|
||||
Database: "mydatabase",
|
||||
Ssl: "disable",
|
||||
}
|
||||
b, err := GetBookmark("../../data", "bookmark")
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, expBookmark, b)
|
||||
}
|
||||
|
||||
_, err = GetBookmark("../../data", "bar")
|
||||
expErrStr := "couldn't find a bookmark with name bar"
|
||||
assert.Equal(t, expErrStr, err.Error())
|
||||
|
||||
_, err = GetBookmark("foo", "bookmark")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func Test_Bookmark_SSHInfoIsEmpty(t *testing.T) {
|
||||
emptySSH := &shared.SSHInfo{
|
||||
Host: "",
|
||||
Port: "",
|
||||
User: "",
|
||||
}
|
||||
populatedSSH := &shared.SSHInfo{
|
||||
Host: "localhost",
|
||||
Port: "8080",
|
||||
User: "postgres",
|
||||
}
|
||||
|
||||
b := Bookmark{Ssh: nil}
|
||||
assert.True(t, b.SSHInfoIsEmpty())
|
||||
|
||||
b = Bookmark{Ssh: emptySSH}
|
||||
assert.True(t, b.SSHInfoIsEmpty())
|
||||
|
||||
b.Ssh = populatedSSH
|
||||
assert.False(t, b.SSHInfoIsEmpty())
|
||||
}
|
||||
|
||||
func Test_ConvertToOptions(t *testing.T) {
|
||||
b := Bookmark{
|
||||
Url: "postgres://username:password@host:port/database?sslmode=disable",
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
User: "postgres",
|
||||
Password: "password",
|
||||
Database: "mydatabase",
|
||||
Ssl: "disable",
|
||||
}
|
||||
|
||||
expOpt := command.Options{
|
||||
Url: "postgres://username:password@host:port/database?sslmode=disable",
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
User: "postgres",
|
||||
Pass: "password",
|
||||
DbName: "mydatabase",
|
||||
Ssl: "disable",
|
||||
}
|
||||
opt := b.ConvertToOptions()
|
||||
assert.Equal(t, expOpt, opt)
|
||||
}
|
||||
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jessevdk/go-flags"
|
||||
|
||||
"github.com/sosedoff/pgweb/pkg/api"
|
||||
"github.com/sosedoff/pgweb/pkg/bookmarks"
|
||||
"github.com/sosedoff/pgweb/pkg/client"
|
||||
"github.com/sosedoff/pgweb/pkg/command"
|
||||
"github.com/sosedoff/pgweb/pkg/connection"
|
||||
"github.com/sosedoff/pgweb/pkg/shared"
|
||||
"github.com/sosedoff/pgweb/pkg/util"
|
||||
)
|
||||
|
||||
var options command.Options
|
||||
|
||||
func exitWithMessage(message string) {
|
||||
fmt.Println("Error:", message)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func initClientUsingBookmark(bookmarkPath, bookmarkName string) (*client.Client, error) {
|
||||
bookmark, err := bookmarks.GetBookmark(bookmarkPath, bookmarkName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opt := bookmark.ConvertToOptions()
|
||||
var connStr string
|
||||
|
||||
if opt.Url != "" { // if the bookmark has url set, use it
|
||||
connStr = opt.Url
|
||||
} else {
|
||||
connStr, err = connection.BuildStringFromOptions(opt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error building connection string: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var ssh *shared.SSHInfo
|
||||
if !bookmark.SSHInfoIsEmpty() {
|
||||
ssh = bookmark.Ssh
|
||||
}
|
||||
|
||||
return client.NewFromUrl(connStr, ssh)
|
||||
}
|
||||
|
||||
func initClient() {
|
||||
if connection.IsBlank(command.Opts) && options.Bookmark == "" {
|
||||
return
|
||||
}
|
||||
|
||||
var cl *client.Client
|
||||
var err error
|
||||
|
||||
if options.Bookmark != "" {
|
||||
cl, err = initClientUsingBookmark(bookmarks.Path(options.BookmarksDir), options.Bookmark)
|
||||
} else {
|
||||
cl, err = client.New()
|
||||
}
|
||||
if err != nil {
|
||||
exitWithMessage(err.Error())
|
||||
}
|
||||
|
||||
if command.Opts.Debug {
|
||||
fmt.Println("Server connection string:", cl.ConnectionString)
|
||||
}
|
||||
|
||||
fmt.Println("Connecting to server...")
|
||||
if err := cl.Test(); err != nil {
|
||||
msg := err.Error()
|
||||
|
||||
// Check if we're trying to connect to the default database.
|
||||
if command.Opts.DbName == "" && command.Opts.Url == "" {
|
||||
// If database does not exist, allow user to connect from the UI.
|
||||
if strings.Contains(msg, "database") && strings.Contains(msg, "does not exist") {
|
||||
fmt.Println("Error:", msg)
|
||||
return
|
||||
}
|
||||
// Do not bail if local server is not running.
|
||||
if strings.Contains(msg, "connection refused") {
|
||||
fmt.Println("Error:", msg)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
exitWithMessage(msg)
|
||||
}
|
||||
|
||||
if !command.Opts.Sessions {
|
||||
fmt.Printf("Connected to %s\n", cl.ServerVersion())
|
||||
}
|
||||
|
||||
fmt.Println("Checking database objects...")
|
||||
_, err = cl.Objects()
|
||||
if err != nil {
|
||||
exitWithMessage(err.Error())
|
||||
}
|
||||
|
||||
api.DbClient = cl
|
||||
}
|
||||
|
||||
func initOptions() {
|
||||
opts, err := command.ParseOptions(os.Args)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case *flags.Error:
|
||||
// no need to print error, flags package already does that
|
||||
default:
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
command.Opts = opts
|
||||
options = opts
|
||||
|
||||
if options.Version {
|
||||
printVersion()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if options.ReadOnly {
|
||||
msg := `------------------------------------------------------
|
||||
SECURITY WARNING: You are running pgweb in read-only mode.
|
||||
This mode is designed for environments where users could potentially delete / change data.
|
||||
For proper read-only access please follow postgresql role management documentation.
|
||||
------------------------------------------------------`
|
||||
fmt.Println(msg)
|
||||
}
|
||||
|
||||
printVersion()
|
||||
}
|
||||
|
||||
func printVersion() {
|
||||
str := fmt.Sprintf("Pgweb v%s", command.Version)
|
||||
if command.GitCommit != "" {
|
||||
str += fmt.Sprintf(" (git: %s)", command.GitCommit)
|
||||
}
|
||||
|
||||
fmt.Println(str)
|
||||
}
|
||||
|
||||
func startServer() {
|
||||
router := gin.Default()
|
||||
|
||||
// Enable HTTP basic authentication only if both user and password are set
|
||||
if options.AuthUser != "" && options.AuthPass != "" {
|
||||
auth := map[string]string{options.AuthUser: options.AuthPass}
|
||||
router.Use(gin.BasicAuth(auth))
|
||||
}
|
||||
|
||||
api.SetupRoutes(router)
|
||||
|
||||
fmt.Println("Starting server...")
|
||||
go func() {
|
||||
err := router.Run(fmt.Sprintf("%v:%v", options.HttpHost, options.HttpPort))
|
||||
if err != nil {
|
||||
fmt.Println("Cant start server:", err)
|
||||
if strings.Contains(err.Error(), "address already in use") {
|
||||
openPage()
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func handleSignals() {
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt, os.Kill)
|
||||
<-c
|
||||
}
|
||||
|
||||
func openPage() {
|
||||
url := fmt.Sprintf("http://%v:%v/%s", options.HttpHost, options.HttpPort, options.Prefix)
|
||||
fmt.Println("To view database open", url, "in browser")
|
||||
|
||||
if options.SkipOpen {
|
||||
return
|
||||
}
|
||||
|
||||
_, err := exec.Command("which", "open").Output()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
exec.Command("open", url).Output()
|
||||
}
|
||||
|
||||
func Run() {
|
||||
initOptions()
|
||||
initClient()
|
||||
|
||||
if api.DbClient != nil {
|
||||
defer api.DbClient.Close()
|
||||
}
|
||||
|
||||
if !options.Debug {
|
||||
gin.SetMode("release")
|
||||
}
|
||||
|
||||
// Print memory usage every 30 seconds with debug flag
|
||||
if options.Debug {
|
||||
util.StartProfiler()
|
||||
}
|
||||
|
||||
// Start session cleanup worker
|
||||
if options.Sessions && !command.Opts.DisableConnectionIdleTimeout {
|
||||
go api.StartSessionCleanup()
|
||||
}
|
||||
|
||||
startServer()
|
||||
openPage()
|
||||
handleSignals()
|
||||
}
|
||||
+149
-17
@@ -2,13 +2,16 @@ package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
neturl "net/url"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/lib/pq"
|
||||
|
||||
"github.com/sosedoff/pgweb/pkg/command"
|
||||
"github.com/sosedoff/pgweb/pkg/connection"
|
||||
"github.com/sosedoff/pgweb/pkg/history"
|
||||
@@ -16,9 +19,21 @@ import (
|
||||
"github.com/sosedoff/pgweb/pkg/statements"
|
||||
)
|
||||
|
||||
var (
|
||||
postgresSignature = regexp.MustCompile(`(?i)postgresql ([\d\.]+)\s`)
|
||||
postgresType = "PostgreSQL"
|
||||
|
||||
cockroachSignature = regexp.MustCompile(`(?i)cockroachdb ccl v([\d\.]+)\s`)
|
||||
cockroachType = "CockroachDB"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
db *sqlx.DB
|
||||
tunnel *Tunnel
|
||||
serverVersion string
|
||||
serverType string
|
||||
lastQueryTime time.Time
|
||||
External bool
|
||||
History []history.Record `json:"history"`
|
||||
ConnectionString string `json:"connection_string"`
|
||||
}
|
||||
@@ -41,7 +56,7 @@ func getSchemaAndTable(str string) (string, string) {
|
||||
}
|
||||
|
||||
func New() (*Client, error) {
|
||||
str, err := connection.BuildString(command.Opts)
|
||||
str, err := connection.BuildStringFromOptions(command.Opts)
|
||||
|
||||
if command.Opts.Debug && str != "" {
|
||||
fmt.Println("Creating a new client for:", str)
|
||||
@@ -62,6 +77,7 @@ func New() (*Client, error) {
|
||||
History: history.New(),
|
||||
}
|
||||
|
||||
client.setServerVersion()
|
||||
return &client, nil
|
||||
}
|
||||
|
||||
@@ -69,6 +85,9 @@ func NewFromUrl(url string, sshInfo *shared.SSHInfo) (*Client, error) {
|
||||
var tunnel *Tunnel
|
||||
|
||||
if sshInfo != nil {
|
||||
if command.Opts.DisableSSH {
|
||||
return nil, fmt.Errorf("ssh connections are disabled")
|
||||
}
|
||||
if command.Opts.Debug {
|
||||
fmt.Println("Opening SSH tunnel for:", sshInfo)
|
||||
}
|
||||
@@ -101,6 +120,11 @@ func NewFromUrl(url string, sshInfo *shared.SSHInfo) (*Client, error) {
|
||||
fmt.Println("Creating a new client for:", url)
|
||||
}
|
||||
|
||||
uri, err := neturl.Parse(url)
|
||||
if err == nil && uri.Path == "" {
|
||||
return nil, fmt.Errorf("Database name is not provided")
|
||||
}
|
||||
|
||||
db, err := sqlx.Open("postgres", url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -113,36 +137,62 @@ func NewFromUrl(url string, sshInfo *shared.SSHInfo) (*Client, error) {
|
||||
History: history.New(),
|
||||
}
|
||||
|
||||
client.setServerVersion()
|
||||
return &client, nil
|
||||
}
|
||||
|
||||
func (client *Client) setServerVersion() {
|
||||
res, err := client.query("SELECT version()")
|
||||
if err != nil || len(res.Rows) < 1 {
|
||||
return
|
||||
}
|
||||
|
||||
version := res.Rows[0][0].(string)
|
||||
|
||||
// Detect postgresql
|
||||
matches := postgresSignature.FindAllStringSubmatch(version, 1)
|
||||
if len(matches) > 0 {
|
||||
client.serverType = postgresType
|
||||
client.serverVersion = matches[0][1]
|
||||
return
|
||||
}
|
||||
|
||||
// Detect cockroachdb
|
||||
matches = cockroachSignature.FindAllStringSubmatch(version, 1)
|
||||
if len(matches) > 0 {
|
||||
client.serverType = cockroachType
|
||||
client.serverVersion = matches[0][1]
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (client *Client) Test() error {
|
||||
return client.db.Ping()
|
||||
}
|
||||
|
||||
func (client *Client) Info() (*Result, error) {
|
||||
return client.query(statements.PG_INFO)
|
||||
return client.query(statements.Info)
|
||||
}
|
||||
|
||||
func (client *Client) Databases() ([]string, error) {
|
||||
return client.fetchRows(statements.PG_DATABASES)
|
||||
return client.fetchRows(statements.Databases)
|
||||
}
|
||||
|
||||
func (client *Client) Schemas() ([]string, error) {
|
||||
return client.fetchRows(statements.PG_SCHEMAS)
|
||||
return client.fetchRows(statements.Schemas)
|
||||
}
|
||||
|
||||
func (client *Client) Objects() (*Result, error) {
|
||||
return client.query(statements.PG_OBJECTS)
|
||||
return client.query(statements.Objects)
|
||||
}
|
||||
|
||||
func (client *Client) Table(table string) (*Result, error) {
|
||||
schema, table := getSchemaAndTable(table)
|
||||
return client.query(statements.PG_TABLE_SCHEMA, schema, table)
|
||||
return client.query(statements.TableSchema, schema, table)
|
||||
}
|
||||
|
||||
func (client *Client) MaterializedView(name string) (*Result, error) {
|
||||
return client.query(statements.PG_MATERIALIZED_VIEW_SCHEMA, name)
|
||||
return client.query(statements.MaterializedView, name)
|
||||
}
|
||||
|
||||
func (client *Client) TableRows(table string, opts RowsOptions) (*Result, error) {
|
||||
@@ -158,7 +208,7 @@ func (client *Client) TableRows(table string, opts RowsOptions) (*Result, error)
|
||||
opts.SortOrder = "ASC"
|
||||
}
|
||||
|
||||
sql += fmt.Sprintf(" ORDER BY %s %s", opts.SortColumn, opts.SortOrder)
|
||||
sql += fmt.Sprintf(` ORDER BY "%s" %s`, opts.SortColumn, opts.SortOrder)
|
||||
}
|
||||
|
||||
if opts.Limit > 0 {
|
||||
@@ -172,9 +222,34 @@ func (client *Client) TableRows(table string, opts RowsOptions) (*Result, error)
|
||||
return client.query(sql)
|
||||
}
|
||||
|
||||
func (client *Client) TableRowsCount(table string, opts RowsOptions) (*Result, error) {
|
||||
func (client *Client) EstimatedTableRowsCount(table string, opts RowsOptions) (*Result, error) {
|
||||
schema, table := getSchemaAndTable(table)
|
||||
sql := fmt.Sprintf(`SELECT COUNT(1) FROM "%s"."%s"`, schema, table)
|
||||
result, err := client.query(statements.EstimatedTableRowCount, schema, table)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// float64 to int64 conversion
|
||||
estimatedRowsCount := result.Rows[0][0].(float64)
|
||||
result.Rows[0] = Row{int64(estimatedRowsCount)}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (client *Client) TableRowsCount(table string, opts RowsOptions) (*Result, error) {
|
||||
// Return postgres estimated rows count on empty filter
|
||||
if opts.Where == "" && client.serverType == postgresType {
|
||||
res, err := client.EstimatedTableRowsCount(table, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n := res.Rows[0][0].(int64)
|
||||
if n >= 100000 {
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
schema, tableName := getSchemaAndTable(table)
|
||||
sql := fmt.Sprintf(`SELECT COUNT(1) FROM "%s"."%s"`, schema, tableName)
|
||||
|
||||
if opts.Where != "" {
|
||||
sql += fmt.Sprintf(" WHERE %s", opts.Where)
|
||||
@@ -184,12 +259,16 @@ func (client *Client) TableRowsCount(table string, opts RowsOptions) (*Result, e
|
||||
}
|
||||
|
||||
func (client *Client) TableInfo(table string) (*Result, error) {
|
||||
return client.query(statements.PG_TABLE_INFO, table)
|
||||
if client.serverType == cockroachType {
|
||||
return client.query(statements.TableInfoCockroach)
|
||||
}
|
||||
schema, table := getSchemaAndTable(table)
|
||||
return client.query(statements.TableInfo, fmt.Sprintf(`"%s"."%s"`, schema, table))
|
||||
}
|
||||
|
||||
func (client *Client) TableIndexes(table string) (*Result, error) {
|
||||
schema, table := getSchemaAndTable(table)
|
||||
res, err := client.query(statements.PG_TABLE_INDEXES, schema, table)
|
||||
res, err := client.query(statements.TableIndexes, schema, table)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -200,7 +279,7 @@ func (client *Client) TableIndexes(table string) (*Result, error) {
|
||||
|
||||
func (client *Client) TableConstraints(table string) (*Result, error) {
|
||||
schema, table := getSchemaAndTable(table)
|
||||
res, err := client.query(statements.PG_TABLE_CONSTRAINTS, schema, table)
|
||||
res, err := client.query(statements.TableConstraints, schema, table)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -211,7 +290,17 @@ func (client *Client) TableConstraints(table string) (*Result, error) {
|
||||
|
||||
// Returns all active queriers on the server
|
||||
func (client *Client) Activity() (*Result, error) {
|
||||
return client.query(statements.PG_ACTIVITY)
|
||||
if client.serverType == cockroachType {
|
||||
return client.query("SHOW QUERIES")
|
||||
}
|
||||
|
||||
version := getMajorMinorVersion(client.serverVersion)
|
||||
query := statements.Activity[version]
|
||||
if query == "" {
|
||||
query = statements.Activity["default"]
|
||||
}
|
||||
|
||||
return client.query(query)
|
||||
}
|
||||
|
||||
func (client *Client) Query(query string) (*Result, error) {
|
||||
@@ -225,7 +314,38 @@ func (client *Client) Query(query string) (*Result, error) {
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (client *Client) SetReadOnlyMode() error {
|
||||
var value string
|
||||
if err := client.db.Get(&value, "SHOW default_transaction_read_only;"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if value == "off" {
|
||||
_, err := client.db.Exec("SET default_transaction_read_only=on;")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (client *Client) ServerVersion() string {
|
||||
return fmt.Sprintf("%s %s", client.serverType, client.serverVersion)
|
||||
}
|
||||
|
||||
func (client *Client) query(query string, args ...interface{}) (*Result, error) {
|
||||
// Update the last usage time
|
||||
defer func() {
|
||||
client.lastQueryTime = time.Now().UTC()
|
||||
}()
|
||||
|
||||
// We're going to force-set transaction mode on every query.
|
||||
// This is needed so that default mode could not be changed by user.
|
||||
if command.Opts.ReadOnly {
|
||||
if err := client.SetReadOnlyMode(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
action := strings.ToLower(strings.Split(query, " ")[0])
|
||||
if action == "update" || action == "delete" {
|
||||
res, err := client.db.Exec(query, args...)
|
||||
@@ -250,9 +370,11 @@ func (client *Client) query(query string, args ...interface{}) (*Result, error)
|
||||
|
||||
rows, err := client.db.Queryx(query, args...)
|
||||
if err != nil {
|
||||
if command.Opts.Debug {
|
||||
log.Println("Failed query:", query, "\nArgs:", args)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
cols, err := rows.Columns()
|
||||
@@ -308,6 +430,16 @@ func (client *Client) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (client *Client) IsIdle() bool {
|
||||
mins := int(time.Since(client.lastQueryTime).Minutes())
|
||||
|
||||
if command.Opts.ConnectionIdleTimeout > 0 {
|
||||
return mins >= command.Opts.ConnectionIdleTimeout
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Fetch all rows as strings for a single column
|
||||
func (client *Client) fetchRows(q string) ([]string, error) {
|
||||
res, err := client.query(q)
|
||||
|
||||
+168
-38
@@ -2,10 +2,14 @@ package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sosedoff/pgweb/pkg/command"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -43,6 +47,11 @@ func getVar(name, def string) string {
|
||||
}
|
||||
|
||||
func initVars() {
|
||||
// We need to load default options to make sure all stuff works
|
||||
if err := command.SetDefaultOptions(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
serverHost = getVar("PGHOST", "localhost")
|
||||
serverPort = getVar("PGPORT", "5432")
|
||||
serverUser = getVar("PGUSER", "postgres")
|
||||
@@ -69,6 +78,9 @@ func onWindows() bool {
|
||||
}
|
||||
|
||||
func setup() {
|
||||
// No pretty JSON for testsm
|
||||
command.Opts.DisablePrettyJson = true
|
||||
|
||||
out, err := exec.Command(
|
||||
testCommands["createdb"],
|
||||
"-U", serverUser,
|
||||
@@ -124,7 +136,7 @@ func teardown() {
|
||||
}
|
||||
}
|
||||
|
||||
func test_NewClientFromUrl(t *testing.T) {
|
||||
func testNewClientFromUrl(t *testing.T) {
|
||||
url := fmt.Sprintf("postgres://%s@%s:%s/%s?sslmode=disable", serverUser, serverHost, serverPort, serverDatabase)
|
||||
client, err := NewFromUrl(url, nil)
|
||||
|
||||
@@ -136,7 +148,7 @@ func test_NewClientFromUrl(t *testing.T) {
|
||||
assert.Equal(t, url, client.ConnectionString)
|
||||
}
|
||||
|
||||
func test_NewClientFromUrl2(t *testing.T) {
|
||||
func testNewClientFromUrl2(t *testing.T) {
|
||||
url := fmt.Sprintf("postgresql://%s@%s:%s/%s?sslmode=disable", serverUser, serverHost, serverPort, serverDatabase)
|
||||
client, err := NewFromUrl(url, nil)
|
||||
|
||||
@@ -148,18 +160,40 @@ func test_NewClientFromUrl2(t *testing.T) {
|
||||
assert.Equal(t, url, client.ConnectionString)
|
||||
}
|
||||
|
||||
func test_Test(t *testing.T) {
|
||||
func testClientIdleTime(t *testing.T) {
|
||||
examples := map[time.Time]bool{
|
||||
time.Now(): false, // Current time
|
||||
time.Now().Add(time.Minute * -30): false, // 30 minutes ago
|
||||
time.Now().Add(time.Minute * -240): true, // 240 minutes ago
|
||||
time.Now().Add(time.Minute * 30): false, // 30 minutes in future
|
||||
time.Now().Add(time.Minute * 128): false, // 128 minutes in future
|
||||
}
|
||||
|
||||
for ts, expected := range examples {
|
||||
testClient.lastQueryTime = ts
|
||||
assert.Equal(t, expected, testClient.IsIdle())
|
||||
}
|
||||
}
|
||||
|
||||
func testTest(t *testing.T) {
|
||||
assert.Equal(t, nil, testClient.Test())
|
||||
}
|
||||
|
||||
func test_Info(t *testing.T) {
|
||||
func testInfo(t *testing.T) {
|
||||
res, err := testClient.Info()
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.NotEqual(t, nil, res)
|
||||
}
|
||||
|
||||
func test_Databases(t *testing.T) {
|
||||
func testActivity(t *testing.T) {
|
||||
res, err := testClient.Activity()
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.NotEqual(t, nil, res)
|
||||
}
|
||||
|
||||
func testDatabases(t *testing.T) {
|
||||
res, err := testClient.Databases()
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
@@ -167,7 +201,7 @@ func test_Databases(t *testing.T) {
|
||||
assert.Contains(t, res, "postgres")
|
||||
}
|
||||
|
||||
func test_Objects(t *testing.T) {
|
||||
func testObjects(t *testing.T) {
|
||||
res, err := testClient.Objects()
|
||||
objects := ObjectsFromResult(res)
|
||||
|
||||
@@ -180,6 +214,7 @@ func test_Objects(t *testing.T) {
|
||||
"customers",
|
||||
"daily_inventory",
|
||||
"distinguished_authors",
|
||||
"dummies",
|
||||
"editions",
|
||||
"employees",
|
||||
"favorite_authors",
|
||||
@@ -198,7 +233,7 @@ func test_Objects(t *testing.T) {
|
||||
}
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, []string{"schema", "name", "type", "owner"}, res.Columns)
|
||||
assert.Equal(t, []string{"schema", "name", "type", "owner", "comment"}, res.Columns)
|
||||
assert.Equal(t, []string{"public"}, mapKeys(objects))
|
||||
assert.Equal(t, tables, objects["public"].Tables)
|
||||
assert.Equal(t, []string{"recent_shipments", "stock_view"}, objects["public"].Views)
|
||||
@@ -212,7 +247,7 @@ func test_Objects(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func test_Table(t *testing.T) {
|
||||
func testTable(t *testing.T) {
|
||||
res, err := testClient.Table("books")
|
||||
|
||||
columns := []string{
|
||||
@@ -222,6 +257,7 @@ func test_Table(t *testing.T) {
|
||||
"character_maximum_length",
|
||||
"character_set_catalog",
|
||||
"column_default",
|
||||
"comment",
|
||||
}
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
@@ -229,7 +265,7 @@ func test_Table(t *testing.T) {
|
||||
assert.Equal(t, 4, len(res.Rows))
|
||||
}
|
||||
|
||||
func test_TableRows(t *testing.T) {
|
||||
func testTableRows(t *testing.T) {
|
||||
res, err := testClient.TableRows("books", RowsOptions{})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
@@ -237,7 +273,7 @@ func test_TableRows(t *testing.T) {
|
||||
assert.Equal(t, 15, len(res.Rows))
|
||||
}
|
||||
|
||||
func test_TableInfo(t *testing.T) {
|
||||
func testTableInfo(t *testing.T) {
|
||||
res, err := testClient.TableInfo("books")
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
@@ -245,7 +281,36 @@ func test_TableInfo(t *testing.T) {
|
||||
assert.Equal(t, 1, len(res.Rows))
|
||||
}
|
||||
|
||||
func test_TableIndexes(t *testing.T) {
|
||||
func testEstimatedTableRowsCount(t *testing.T) {
|
||||
var count int64 = 15
|
||||
res, err := testClient.EstimatedTableRowsCount("books", RowsOptions{})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, []string{"reltuples"}, res.Columns)
|
||||
assert.Equal(t, []Row{Row{count}}, res.Rows)
|
||||
}
|
||||
|
||||
func testTableRowsCount(t *testing.T) {
|
||||
var count int64 = 15
|
||||
res, err := testClient.TableRowsCount("books", RowsOptions{})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, []string{"count"}, res.Columns)
|
||||
assert.Equal(t, []Row{Row{count}}, res.Rows)
|
||||
}
|
||||
|
||||
func testTableRowsCountWithLargeTable(t *testing.T) {
|
||||
var count int64 = 100010
|
||||
testClient.db.MustExec(`CREATE TABLE large_table AS SELECT s FROM generate_Series(1,100010) s;`)
|
||||
testClient.db.MustExec(`VACUUM large_table;`)
|
||||
res, err := testClient.TableRowsCount("large_table", RowsOptions{})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, []string{"reltuples"}, res.Columns)
|
||||
assert.Equal(t, []Row{Row{count}}, res.Rows)
|
||||
}
|
||||
|
||||
func testTableIndexes(t *testing.T) {
|
||||
res, err := testClient.TableIndexes("books")
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
@@ -253,15 +318,39 @@ func test_TableIndexes(t *testing.T) {
|
||||
assert.Equal(t, 2, len(res.Rows))
|
||||
}
|
||||
|
||||
func test_TableConstraints(t *testing.T) {
|
||||
func testTableConstraints(t *testing.T) {
|
||||
res, err := testClient.TableConstraints("editions")
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, 1, len(res.Columns))
|
||||
assert.Equal(t, 2, len(res.Rows))
|
||||
assert.Equal(t, []string{"name", "definition"}, res.Columns)
|
||||
assert.Equal(t, Row{"pkey", "PRIMARY KEY (isbn)"}, res.Rows[0])
|
||||
assert.Equal(t, Row{"integrity", "CHECK (book_id IS NOT NULL AND edition IS NOT NULL)"}, res.Rows[1])
|
||||
}
|
||||
|
||||
func test_Query(t *testing.T) {
|
||||
func testTableNameWithCamelCase(t *testing.T) {
|
||||
testClient.db.MustExec(`CREATE TABLE "exampleTable" (id int, name varchar);`)
|
||||
testClient.db.MustExec(`INSERT INTO "exampleTable" (id, name) VALUES (1, 'foo'), (2, 'bar');`)
|
||||
|
||||
_, err := testClient.Table("exampleTable")
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = testClient.TableInfo("exampleTable")
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = testClient.TableConstraints("exampleTable")
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = testClient.TableIndexes("exampleTable")
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = testClient.TableRowsCount("exampleTable", RowsOptions{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = testClient.EstimatedTableRowsCount("exampleTable", RowsOptions{})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func testQuery(t *testing.T) {
|
||||
res, err := testClient.Query("SELECT * FROM books")
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
@@ -269,7 +358,7 @@ func test_Query(t *testing.T) {
|
||||
assert.Equal(t, 15, len(res.Rows))
|
||||
}
|
||||
|
||||
func test_QueryError(t *testing.T) {
|
||||
func testQueryError(t *testing.T) {
|
||||
res, err := testClient.Query("SELCT * FROM books")
|
||||
|
||||
assert.NotEqual(t, nil, err)
|
||||
@@ -277,7 +366,7 @@ func test_QueryError(t *testing.T) {
|
||||
assert.Equal(t, true, res == nil)
|
||||
}
|
||||
|
||||
func test_QueryInvalidTable(t *testing.T) {
|
||||
func testQueryInvalidTable(t *testing.T) {
|
||||
res, err := testClient.Query("SELECT * FROM books2")
|
||||
|
||||
assert.NotEqual(t, nil, err)
|
||||
@@ -285,7 +374,25 @@ func test_QueryInvalidTable(t *testing.T) {
|
||||
assert.Equal(t, true, res == nil)
|
||||
}
|
||||
|
||||
func test_ResultCsv(t *testing.T) {
|
||||
func testTableRowsOrderEscape(t *testing.T) {
|
||||
rows, err := testClient.TableRows("dummies", RowsOptions{SortColumn: "isDummy"})
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, 2, len(rows.Rows))
|
||||
|
||||
rows, err = testClient.TableRows("dummies", RowsOptions{SortColumn: "isdummy"})
|
||||
assert.NotEqual(t, nil, err)
|
||||
assert.Equal(t, `pq: column "isdummy" does not exist`, err.Error())
|
||||
assert.Equal(t, true, rows == nil)
|
||||
}
|
||||
|
||||
func testResultJSON(t *testing.T) {
|
||||
result, err := testClient.Query("SELECT 'NaN'::float AS value;")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `[{"value":null}]`, string(result.JSON()))
|
||||
}
|
||||
|
||||
func testResultCsv(t *testing.T) {
|
||||
res, _ := testClient.Query("SELECT * FROM books ORDER BY id ASC LIMIT 1")
|
||||
csv := res.CSV()
|
||||
|
||||
@@ -294,7 +401,7 @@ func test_ResultCsv(t *testing.T) {
|
||||
assert.Equal(t, expected, string(csv))
|
||||
}
|
||||
|
||||
func test_History(t *testing.T) {
|
||||
func testHistory(t *testing.T) {
|
||||
_, err := testClient.Query("SELECT * FROM books WHERE id = 12345")
|
||||
query := testClient.History[len(testClient.History)-1].Query
|
||||
|
||||
@@ -302,7 +409,7 @@ func test_History(t *testing.T) {
|
||||
assert.Equal(t, "SELECT * FROM books WHERE id = 12345", query)
|
||||
}
|
||||
|
||||
func test_HistoryError(t *testing.T) {
|
||||
func testHistoryError(t *testing.T) {
|
||||
_, err := testClient.Query("SELECT * FROM books123")
|
||||
query := testClient.History[len(testClient.History)-1].Query
|
||||
|
||||
@@ -310,7 +417,7 @@ func test_HistoryError(t *testing.T) {
|
||||
assert.NotEqual(t, "SELECT * FROM books123", query)
|
||||
}
|
||||
|
||||
func test_HistoryUniqueness(t *testing.T) {
|
||||
func testHistoryUniqueness(t *testing.T) {
|
||||
url := fmt.Sprintf("postgres://%s@%s:%s/%s?sslmode=disable", serverUser, serverHost, serverPort, serverDatabase)
|
||||
client, _ := NewFromUrl(url, nil)
|
||||
|
||||
@@ -321,9 +428,21 @@ func test_HistoryUniqueness(t *testing.T) {
|
||||
assert.Equal(t, "SELECT * FROM books WHERE id = 1", client.History[0].Query)
|
||||
}
|
||||
|
||||
func testReadOnlyMode(t *testing.T) {
|
||||
url := fmt.Sprintf("postgres://%s@%s:%s/%s?sslmode=disable", serverUser, serverHost, serverPort, serverDatabase)
|
||||
client, _ := NewFromUrl(url, nil)
|
||||
|
||||
err := client.SetReadOnlyMode()
|
||||
assert.Equal(t, nil, err)
|
||||
|
||||
_, err = client.Query("CREATE TABLE foobar(id integer);")
|
||||
assert.NotNil(t, err)
|
||||
assert.Contains(t, err.Error(), "in a read-only transaction")
|
||||
}
|
||||
|
||||
func TestAll(t *testing.T) {
|
||||
if onWindows() {
|
||||
// Dont have access to windows machines at the moment...
|
||||
t.Log("Unit testing on Windows platform is not supported.")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -333,22 +452,33 @@ func TestAll(t *testing.T) {
|
||||
setup()
|
||||
setupClient()
|
||||
|
||||
test_NewClientFromUrl(t)
|
||||
test_Test(t)
|
||||
test_Info(t)
|
||||
test_Databases(t)
|
||||
test_Objects(t)
|
||||
test_Table(t)
|
||||
test_TableRows(t)
|
||||
test_TableInfo(t)
|
||||
test_TableIndexes(t)
|
||||
test_TableConstraints(t)
|
||||
test_Query(t)
|
||||
test_QueryError(t)
|
||||
test_QueryInvalidTable(t)
|
||||
test_ResultCsv(t)
|
||||
test_History(t)
|
||||
test_HistoryError(t)
|
||||
testNewClientFromUrl(t)
|
||||
testClientIdleTime(t)
|
||||
testTest(t)
|
||||
testInfo(t)
|
||||
testActivity(t)
|
||||
testDatabases(t)
|
||||
testObjects(t)
|
||||
testTable(t)
|
||||
testTableRows(t)
|
||||
testTableInfo(t)
|
||||
testEstimatedTableRowsCount(t)
|
||||
testTableRowsCount(t)
|
||||
testTableRowsCountWithLargeTable(t)
|
||||
testTableIndexes(t)
|
||||
testTableConstraints(t)
|
||||
testTableNameWithCamelCase(t)
|
||||
testQuery(t)
|
||||
testQueryError(t)
|
||||
testQueryInvalidTable(t)
|
||||
testTableRowsOrderEscape(t)
|
||||
testResultJSON(t)
|
||||
testResultCsv(t)
|
||||
testHistory(t)
|
||||
testHistoryUniqueness(t)
|
||||
testHistoryError(t)
|
||||
testReadOnlyMode(t)
|
||||
testDumpExport(t)
|
||||
|
||||
teardownClient()
|
||||
teardown()
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
type Dump struct {
|
||||
Table string
|
||||
}
|
||||
|
||||
func (d *Dump) CanExport() bool {
|
||||
err := exec.Command("pg_dump", "--version").Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (d *Dump) Export(url string, writer io.Writer) error {
|
||||
errOutput := bytes.NewBuffer(nil)
|
||||
|
||||
opts := []string{
|
||||
"--no-owner", // skip restoration of object ownership in plain-text format
|
||||
"--clean", // clean (drop) database objects before recreating
|
||||
"--compress", "6", // compression level for compressed formats
|
||||
}
|
||||
|
||||
if d.Table != "" {
|
||||
opts = append(opts, []string{"--table", d.Table}...)
|
||||
}
|
||||
|
||||
opts = append(opts, url)
|
||||
|
||||
cmd := exec.Command("pg_dump", opts...)
|
||||
cmd.Stdout = writer
|
||||
cmd.Stderr = errOutput
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("error: %s. output: %s", err.Error(), errOutput.Bytes())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func testDumpExport(t *testing.T) {
|
||||
url := fmt.Sprintf("postgres://%s@%s:%s/%s?sslmode=disable", serverUser, serverHost, serverPort, serverDatabase)
|
||||
|
||||
savePath := "/tmp/dump.sql.gz"
|
||||
os.Remove(savePath)
|
||||
|
||||
saveFile, err := os.Create(savePath)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
defer func() {
|
||||
saveFile.Close()
|
||||
os.Remove(savePath)
|
||||
}()
|
||||
|
||||
dump := Dump{}
|
||||
|
||||
// Test for pg_dump presence
|
||||
assert.True(t, dump.CanExport())
|
||||
|
||||
// Test full db dump
|
||||
err = dump.Export(url, saveFile)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test nonexistent database
|
||||
invalidUrl := fmt.Sprintf("postgres://%s@%s:%s/%s?sslmode=disable", serverUser, serverHost, serverPort, "foobar")
|
||||
err = dump.Export(invalidUrl, saveFile)
|
||||
assert.Contains(t, err.Error(), `database "foobar" does not exist`)
|
||||
|
||||
// Test dump of non existent db
|
||||
dump = Dump{Table: "foobar"}
|
||||
err = dump.Export(url, saveFile)
|
||||
assert.NotNil(t, err)
|
||||
assert.Contains(t, err.Error(), "pg_dump: no matching tables were found")
|
||||
}
|
||||
+19
-1
@@ -5,9 +5,12 @@ import (
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/sosedoff/pgweb/pkg/command"
|
||||
)
|
||||
|
||||
type Row []interface{}
|
||||
@@ -49,6 +52,14 @@ func (res *Result) PrepareBigints() {
|
||||
}
|
||||
case reflect.Float64:
|
||||
val := col.(float64)
|
||||
|
||||
// json.Marshal panics when dealing with NaN/Inf values
|
||||
// issue: https://github.com/golang/go/issues/25721
|
||||
if math.IsNaN(val) {
|
||||
res.Rows[i][j] = nil
|
||||
break
|
||||
}
|
||||
|
||||
if val < -999999999999999 || val > 999999999999999 {
|
||||
res.Rows[i][j] = strconv.FormatFloat(val, 'e', -1, 64)
|
||||
}
|
||||
@@ -108,7 +119,14 @@ func (res *Result) CSV() []byte {
|
||||
}
|
||||
|
||||
func (res *Result) JSON() []byte {
|
||||
data, _ := json.Marshal(res.Format())
|
||||
var data []byte
|
||||
|
||||
if command.Opts.DisablePrettyJson {
|
||||
data, _ = json.Marshal(res.Format())
|
||||
} else {
|
||||
data, _ = json.MarshalIndent(res.Format(), "", " ")
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
|
||||
+35
-5
@@ -10,6 +10,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
@@ -18,10 +19,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
PORT_START = 29168
|
||||
PORT_LIMIT = 500
|
||||
portStart = 29168
|
||||
portLimit = 500
|
||||
)
|
||||
|
||||
// Tunnel represents the connection between local and remote server
|
||||
type Tunnel struct {
|
||||
TargetHost string
|
||||
TargetPort string
|
||||
@@ -36,6 +38,14 @@ func privateKeyPath() string {
|
||||
return os.Getenv("HOME") + "/.ssh/id_rsa"
|
||||
}
|
||||
|
||||
func expandKeyPath(path string) string {
|
||||
home := os.Getenv("HOME")
|
||||
if home == "" {
|
||||
return path
|
||||
}
|
||||
return strings.Replace(path, "~", home, 1)
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
@@ -53,7 +63,14 @@ func parsePrivateKey(keyPath string) (ssh.Signer, error) {
|
||||
func makeConfig(info *shared.SSHInfo) (*ssh.ClientConfig, error) {
|
||||
methods := []ssh.AuthMethod{}
|
||||
|
||||
keyPath := privateKeyPath()
|
||||
// Try to use user-provided key, fallback to system default key
|
||||
keyPath := info.Key
|
||||
if keyPath == "" {
|
||||
keyPath = privateKeyPath()
|
||||
} else {
|
||||
keyPath = expandKeyPath(keyPath)
|
||||
}
|
||||
|
||||
if fileExists(keyPath) {
|
||||
key, err := parsePrivateKey(keyPath)
|
||||
if err != nil {
|
||||
@@ -65,7 +82,16 @@ func makeConfig(info *shared.SSHInfo) (*ssh.ClientConfig, error) {
|
||||
|
||||
methods = append(methods, ssh.Password(info.Password))
|
||||
|
||||
return &ssh.ClientConfig{User: info.User, Auth: methods}, nil
|
||||
cfg := &ssh.ClientConfig{
|
||||
User: info.User,
|
||||
Auth: methods,
|
||||
Timeout: time.Second * 10,
|
||||
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (tunnel *Tunnel) sshEndpoint() string {
|
||||
@@ -99,6 +125,7 @@ func (tunnel *Tunnel) handleConnection(local net.Conn) {
|
||||
local.Close()
|
||||
}
|
||||
|
||||
// Close closes the tunnel connection
|
||||
func (tunnel *Tunnel) Close() {
|
||||
if tunnel.Client != nil {
|
||||
tunnel.Client.Close()
|
||||
@@ -109,6 +136,7 @@ func (tunnel *Tunnel) Close() {
|
||||
}
|
||||
}
|
||||
|
||||
// Configure establishes the tunnel between localhost and remote machine
|
||||
func (tunnel *Tunnel) Configure() error {
|
||||
config, err := makeConfig(tunnel.SSHInfo)
|
||||
if err != nil {
|
||||
@@ -131,6 +159,7 @@ func (tunnel *Tunnel) Configure() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start starts the connection handler loop
|
||||
func (tunnel *Tunnel) Start() {
|
||||
defer tunnel.Close()
|
||||
|
||||
@@ -144,13 +173,14 @@ func (tunnel *Tunnel) Start() {
|
||||
}
|
||||
}
|
||||
|
||||
// NewTunnel instantiates a new tunnel struct from given ssh info
|
||||
func NewTunnel(sshInfo *shared.SSHInfo, dbUrl string) (*Tunnel, error) {
|
||||
uri, err := url.Parse(dbUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
listenPort, err := connection.AvailablePort(PORT_START, PORT_LIMIT)
|
||||
listenPort, err := connection.FindAvailablePort(portStart, portLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Get short version from the string
|
||||
// Example: 10.2.3.1 -> 10.2
|
||||
func getMajorMinorVersion(str string) string {
|
||||
chunks := strings.Split(str, ".")
|
||||
if len(chunks) == 0 {
|
||||
return str
|
||||
}
|
||||
return strings.Join(chunks[0:2], ".")
|
||||
}
|
||||
+110
-24
@@ -1,50 +1,136 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/user"
|
||||
"strings"
|
||||
|
||||
"github.com/jessevdk/go-flags"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Version bool `short:"v" long:"version" description:"Print version"`
|
||||
Debug bool `short:"d" long:"debug" description:"Enable debugging mode" default:"false"`
|
||||
Url string `long:"url" description:"Database connection string"`
|
||||
Host string `long:"host" description:"Server hostname or IP"`
|
||||
Port int `long:"port" description:"Server port" default:"5432"`
|
||||
User string `long:"user" description:"Database user"`
|
||||
Pass string `long:"pass" description:"Password for user"`
|
||||
DbName string `long:"db" description:"Database name"`
|
||||
Ssl string `long:"ssl" description:"SSL option"`
|
||||
HttpHost string `long:"bind" description:"HTTP server host" default:"localhost"`
|
||||
HttpPort uint `long:"listen" description:"HTTP server listen port" default:"8081"`
|
||||
AuthUser string `long:"auth-user" description:"HTTP basic auth user"`
|
||||
AuthPass string `long:"auth-pass" description:"HTTP basic auth password"`
|
||||
SkipOpen bool `short:"s" long:"skip-open" description:"Skip browser open on start"`
|
||||
Sessions bool `long:"sessions" description:"Enable multiple database sessions" default:"false"`
|
||||
Prefix string `long:"prefix" description:"Add a url prefix"`
|
||||
Version bool `short:"v" long:"version" description:"Print version"`
|
||||
Debug bool `short:"d" long:"debug" description:"Enable debugging mode"`
|
||||
Url string `long:"url" description:"Database connection string"`
|
||||
Host string `long:"host" description:"Server hostname or IP" default:"localhost"`
|
||||
Port int `long:"port" description:"Server port" default:"5432"`
|
||||
User string `long:"user" description:"Database user"`
|
||||
Pass string `long:"pass" description:"Password for user"`
|
||||
DbName string `long:"db" description:"Database name"`
|
||||
Ssl string `long:"ssl" description:"SSL option"`
|
||||
HttpHost string `long:"bind" description:"HTTP server host" default:"localhost"`
|
||||
HttpPort uint `long:"listen" description:"HTTP server listen port" default:"8081"`
|
||||
AuthUser string `long:"auth-user" description:"HTTP basic auth user"`
|
||||
AuthPass string `long:"auth-pass" description:"HTTP basic auth password"`
|
||||
SkipOpen bool `short:"s" long:"skip-open" description:"Skip browser open on start"`
|
||||
Sessions bool `long:"sessions" description:"Enable multiple database sessions"`
|
||||
Prefix string `long:"prefix" description:"Add a url prefix"`
|
||||
ReadOnly bool `long:"readonly" description:"Run database connection in readonly mode"`
|
||||
LockSession bool `long:"lock-session" description:"Lock session to a single database connection"`
|
||||
Bookmark string `short:"b" long:"bookmark" description:"Bookmark to use for connection. Bookmark files are stored under $HOME/.pgweb/bookmarks/*.toml" default:""`
|
||||
BookmarksDir string `long:"bookmarks-dir" description:"Overrides default directory for bookmark files to search" default:""`
|
||||
DisablePrettyJson bool `long:"no-pretty-json" description:"Disable JSON formatting feature for result export"`
|
||||
DisableSSH bool `long:"no-ssh" description:"Disable database connections via SSH"`
|
||||
ConnectBackend string `long:"connect-backend" description:"Enable database authentication through a third party backend"`
|
||||
ConnectToken string `long:"connect-token" description:"Authentication token for the third-party connect backend"`
|
||||
ConnectHeaders string `long:"connect-headers" description:"List of headers to pass to the connect backend"`
|
||||
DisableConnectionIdleTimeout bool `long:"no-idle-timeout" description:"Disable connection idle timeout"`
|
||||
ConnectionIdleTimeout int `long:"idle-timeout" description:"Set connection idle timeout in minutes" default:"180"`
|
||||
Cors bool `long:"cors" description:"Enable Cross-Origin Resource Sharing (CORS)"`
|
||||
CorsOrigin string `long:"cors-origin" description:"Allowed CORS origins" default:"*"`
|
||||
}
|
||||
|
||||
var Opts Options
|
||||
|
||||
func ParseOptions() error {
|
||||
_, err := flags.ParseArgs(&Opts, os.Args)
|
||||
func ParseOptions(args []string) (Options, error) {
|
||||
var opts = Options{}
|
||||
|
||||
_, err := flags.ParseArgs(&opts, args)
|
||||
if err != nil {
|
||||
return err
|
||||
return opts, err
|
||||
}
|
||||
|
||||
if Opts.Url == "" {
|
||||
Opts.Url = os.Getenv("DATABASE_URL")
|
||||
if opts.Url == "" {
|
||||
opts.Url = os.Getenv("DATABASE_URL")
|
||||
}
|
||||
|
||||
if opts.Prefix == "" {
|
||||
opts.Prefix = os.Getenv("URL_PREFIX")
|
||||
}
|
||||
|
||||
// Handle edge case where pgweb is started with a default host `localhost` and no user.
|
||||
// When user is not set the `lib/pq` connection will fail and cause pgweb's termination.
|
||||
if (opts.Host == "localhost" || opts.Host == "127.0.0.1") && opts.User == "" {
|
||||
if username := GetCurrentUser(); username != "" {
|
||||
opts.User = username
|
||||
} else {
|
||||
opts.Host = ""
|
||||
}
|
||||
}
|
||||
|
||||
if os.Getenv("SESSIONS") != "" {
|
||||
Opts.Sessions = true
|
||||
opts.Sessions = true
|
||||
}
|
||||
|
||||
if Opts.Prefix != "" && !strings.Contains(Opts.Prefix, "/") {
|
||||
Opts.Prefix = Opts.Prefix + "/"
|
||||
if os.Getenv("LOCK_SESSION") != "" {
|
||||
opts.LockSession = true
|
||||
opts.Sessions = false
|
||||
}
|
||||
|
||||
if opts.Sessions || opts.ConnectBackend != "" {
|
||||
opts.Bookmark = ""
|
||||
opts.Url = ""
|
||||
opts.Host = ""
|
||||
opts.User = ""
|
||||
opts.Pass = ""
|
||||
opts.DbName = ""
|
||||
opts.Ssl = ""
|
||||
}
|
||||
|
||||
if opts.Prefix != "" && !strings.Contains(opts.Prefix, "/") {
|
||||
opts.Prefix = opts.Prefix + "/"
|
||||
}
|
||||
|
||||
if opts.AuthUser == "" && os.Getenv("AUTH_USER") != "" {
|
||||
opts.AuthUser = os.Getenv("AUTH_USER")
|
||||
}
|
||||
|
||||
if opts.AuthPass == "" && os.Getenv("AUTH_PASS") != "" {
|
||||
opts.AuthPass = os.Getenv("AUTH_PASS")
|
||||
}
|
||||
|
||||
if opts.ConnectBackend != "" {
|
||||
if !opts.Sessions {
|
||||
return opts, errors.New("--sessions flag must be set")
|
||||
}
|
||||
if opts.ConnectToken == "" {
|
||||
return opts, errors.New("--connect-token flag must be set")
|
||||
}
|
||||
} else {
|
||||
if opts.ConnectToken != "" || opts.ConnectHeaders != "" {
|
||||
return opts, errors.New("--connect-backend flag must be set")
|
||||
}
|
||||
}
|
||||
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// SetDefaultOptions parses and assigns the options
|
||||
func SetDefaultOptions() error {
|
||||
opts, err := ParseOptions([]string{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
Opts = opts
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCurrentUser returns a current user name
|
||||
func GetCurrentUser() string {
|
||||
u, _ := user.Current()
|
||||
if u != nil {
|
||||
return u.Username
|
||||
}
|
||||
return os.Getenv("USER")
|
||||
}
|
||||
|
||||
+32
-23
@@ -1,38 +1,47 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Options(t *testing.T) {
|
||||
err := ParseOptions()
|
||||
|
||||
func TestParseOptions(t *testing.T) {
|
||||
// Test default behavior
|
||||
opts, err := ParseOptions([]string{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, false, Opts.Sessions)
|
||||
assert.Equal(t, "", Opts.Prefix)
|
||||
}
|
||||
assert.Equal(t, false, opts.Sessions)
|
||||
assert.Equal(t, "", opts.Prefix)
|
||||
assert.Equal(t, "", opts.ConnectToken)
|
||||
assert.Equal(t, "", opts.ConnectHeaders)
|
||||
assert.Equal(t, false, opts.DisableSSH)
|
||||
assert.Equal(t, false, opts.DisablePrettyJson)
|
||||
assert.Equal(t, false, opts.DisableConnectionIdleTimeout)
|
||||
assert.Equal(t, 180, opts.ConnectionIdleTimeout)
|
||||
assert.Equal(t, false, opts.Cors)
|
||||
assert.Equal(t, "*", opts.CorsOrigin)
|
||||
|
||||
func Test_SessionsOption(t *testing.T) {
|
||||
oldargs := os.Args
|
||||
defer func() { os.Args = oldargs }()
|
||||
// Test sessions
|
||||
opts, err = ParseOptions([]string{"--sessions", "1"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, true, opts.Sessions)
|
||||
|
||||
os.Args = []string{"--sessions", "1"}
|
||||
assert.NoError(t, ParseOptions())
|
||||
assert.Equal(t, true, Opts.Sessions)
|
||||
}
|
||||
// Test url prefix
|
||||
opts, err = ParseOptions([]string{"--prefix", "pgweb"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pgweb/", opts.Prefix)
|
||||
|
||||
func Test_PrefixOption(t *testing.T) {
|
||||
oldargs := os.Args
|
||||
defer func() { os.Args = oldargs }()
|
||||
opts, err = ParseOptions([]string{"--prefix", "pgweb/"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pgweb/", opts.Prefix)
|
||||
|
||||
os.Args = []string{"--prefix", "pgweb"}
|
||||
assert.NoError(t, ParseOptions())
|
||||
assert.Equal(t, "pgweb/", Opts.Prefix)
|
||||
// Test connect backend options
|
||||
opts, err = ParseOptions([]string{"--connect-backend", "test"})
|
||||
assert.EqualError(t, err, "--sessions flag must be set")
|
||||
|
||||
os.Args = []string{"--prefix", "pgweb/"}
|
||||
assert.NoError(t, ParseOptions())
|
||||
assert.Equal(t, "pgweb/", Opts.Prefix)
|
||||
opts, err = ParseOptions([]string{"--connect-backend", "test", "--sessions"})
|
||||
assert.EqualError(t, err, "--connect-token flag must be set")
|
||||
|
||||
opts, err = ParseOptions([]string{"--connect-backend", "test", "--sessions", "--connect-token", "token"})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package command
|
||||
|
||||
const VERSION = "0.9.3"
|
||||
const (
|
||||
Version = "0.11.2"
|
||||
)
|
||||
|
||||
var (
|
||||
GitCommit string
|
||||
|
||||
@@ -11,6 +11,13 @@ import (
|
||||
"github.com/sosedoff/pgweb/pkg/command"
|
||||
)
|
||||
|
||||
// Common errors
|
||||
var (
|
||||
errCantDetectUser = errors.New("Could not detect default username")
|
||||
errInvalidURLFormat = errors.New("Invalid URL. Valid format: postgres://user:password@host:port/db?sslmode=mode")
|
||||
)
|
||||
|
||||
// currentUser returns a current user name
|
||||
func currentUser() (string, error) {
|
||||
u, err := user.Current()
|
||||
if err == nil {
|
||||
@@ -22,44 +29,73 @@ func currentUser() (string, error) {
|
||||
return name, nil
|
||||
}
|
||||
|
||||
return "", errors.New("Unable to detect OS user")
|
||||
return "", errCantDetectUser
|
||||
}
|
||||
|
||||
func FormatUrl(opts command.Options) (string, error) {
|
||||
// Check if connection url has a correct postgres prefix
|
||||
func hasValidPrefix(str string) bool {
|
||||
return strings.HasPrefix(str, "postgres://") || strings.HasPrefix(str, "postgresql://")
|
||||
}
|
||||
|
||||
// Extract all query vals and return as a map
|
||||
func valsFromQuery(vals neturl.Values) map[string]string {
|
||||
result := map[string]string{}
|
||||
for k, v := range vals {
|
||||
result[strings.ToLower(k)] = v[0]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FormatURL reformats the existing connection string
|
||||
func FormatURL(opts command.Options) (string, error) {
|
||||
url := opts.Url
|
||||
|
||||
// Make sure to only accept urls in a standard format
|
||||
if !strings.HasPrefix(url, "postgres://") && !strings.HasPrefix(url, "postgresql://") {
|
||||
return "", errors.New("Invalid URL. Valid format: postgres://user:password@host:port/db?sslmode=mode")
|
||||
// Validate connection string prefix
|
||||
if !hasValidPrefix(url) {
|
||||
return "", errInvalidURLFormat
|
||||
}
|
||||
|
||||
// Special handling for local connections
|
||||
if strings.Contains(url, "localhost") || strings.Contains(url, "127.0.0.1") {
|
||||
if !strings.Contains(url, "?sslmode") {
|
||||
if opts.Ssl == "" {
|
||||
url += fmt.Sprintf("?sslmode=%s", "disable")
|
||||
} else {
|
||||
url += fmt.Sprintf("?sslmode=%s", opts.Ssl)
|
||||
// Validate the URL
|
||||
uri, err := neturl.Parse(url)
|
||||
if err != nil {
|
||||
return "", errInvalidURLFormat
|
||||
}
|
||||
|
||||
// Get query params
|
||||
params := valsFromQuery(uri.Query())
|
||||
|
||||
// Determine if we need to specify sslmode if it's missing
|
||||
if params["sslmode"] == "" {
|
||||
if opts.Ssl == "" {
|
||||
// Only modify sslmode for local connections
|
||||
if strings.Contains(uri.Host, "localhost") || strings.Contains(uri.Host, "127.0.0.1") {
|
||||
params["sslmode"] = "disable"
|
||||
}
|
||||
} else {
|
||||
params["sslmode"] = opts.Ssl
|
||||
}
|
||||
}
|
||||
|
||||
// Append sslmode parameter only if its defined as a flag and not present
|
||||
// in the connection string.
|
||||
if !strings.Contains(url, "?sslmode") && opts.Ssl != "" {
|
||||
url += fmt.Sprintf("?sslmode=%s", opts.Ssl)
|
||||
// Rebuild query params
|
||||
query := neturl.Values{}
|
||||
for k, v := range params {
|
||||
query.Add(k, v)
|
||||
}
|
||||
uri.RawQuery = query.Encode()
|
||||
|
||||
return url, nil
|
||||
return uri.String(), nil
|
||||
}
|
||||
|
||||
// IsBlank returns true if command options do not contain connection details
|
||||
func IsBlank(opts command.Options) bool {
|
||||
return opts.Host == "" && opts.User == "" && opts.DbName == "" && opts.Url == ""
|
||||
}
|
||||
|
||||
func BuildString(opts command.Options) (string, error) {
|
||||
// BuildStringFromOptions returns a new connection string built from options
|
||||
func BuildStringFromOptions(opts command.Options) (string, error) {
|
||||
// If connection string is provided we just use that
|
||||
if opts.Url != "" {
|
||||
return FormatUrl(opts)
|
||||
return FormatURL(opts)
|
||||
}
|
||||
|
||||
// Try to detect user from current OS user
|
||||
@@ -71,31 +107,22 @@ func BuildString(opts command.Options) (string, error) {
|
||||
}
|
||||
|
||||
// Disable ssl for localhost connections, most users have it disabled
|
||||
if opts.Host == "localhost" || opts.Host == "127.0.0.1" {
|
||||
if opts.Ssl == "" {
|
||||
opts.Ssl = "disable"
|
||||
}
|
||||
}
|
||||
|
||||
url := "postgres://"
|
||||
|
||||
if opts.User != "" {
|
||||
url += opts.User
|
||||
}
|
||||
|
||||
if opts.Pass != "" {
|
||||
url += fmt.Sprintf(":%s", neturl.QueryEscape(opts.Pass))
|
||||
}
|
||||
|
||||
url += fmt.Sprintf("@%s:%d", opts.Host, opts.Port)
|
||||
|
||||
if opts.DbName != "" {
|
||||
url += fmt.Sprintf("/%s", opts.DbName)
|
||||
if opts.Ssl == "" && (opts.Host == "localhost" || opts.Host == "127.0.0.1") {
|
||||
opts.Ssl = "disable"
|
||||
}
|
||||
|
||||
query := neturl.Values{}
|
||||
if opts.Ssl != "" {
|
||||
url += fmt.Sprintf("?sslmode=%s", opts.Ssl)
|
||||
query.Add("sslmode", opts.Ssl)
|
||||
}
|
||||
|
||||
return url, nil
|
||||
url := neturl.URL{
|
||||
Scheme: "postgres",
|
||||
Host: fmt.Sprintf("%v:%v", opts.Host, opts.Port),
|
||||
User: neturl.UserPassword(opts.User, opts.Pass),
|
||||
Path: fmt.Sprintf("/%s", opts.DbName),
|
||||
RawQuery: query.Encode(),
|
||||
}
|
||||
|
||||
return url.String(), nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package connection
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os/user"
|
||||
"testing"
|
||||
|
||||
@@ -13,12 +14,13 @@ func Test_Invalid_Url(t *testing.T) {
|
||||
opts := command.Options{}
|
||||
examples := []string{
|
||||
"postgre://foobar",
|
||||
"tcp://blah",
|
||||
"foobar",
|
||||
}
|
||||
|
||||
for _, val := range examples {
|
||||
opts.Url = val
|
||||
str, err := BuildString(opts)
|
||||
str, err := BuildStringFromOptions(opts)
|
||||
|
||||
assert.Equal(t, "", str)
|
||||
assert.Error(t, err)
|
||||
@@ -28,14 +30,14 @@ func Test_Invalid_Url(t *testing.T) {
|
||||
|
||||
func Test_Valid_Url(t *testing.T) {
|
||||
url := "postgres://myhost/database"
|
||||
str, err := BuildString(command.Options{Url: url})
|
||||
str, err := BuildStringFromOptions(command.Options{Url: url})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, url, str)
|
||||
}
|
||||
|
||||
func Test_Url_And_Ssl_Flag(t *testing.T) {
|
||||
str, err := BuildString(command.Options{
|
||||
str, err := BuildStringFromOptions(command.Options{
|
||||
Url: "postgres://myhost/database",
|
||||
Ssl: "disable",
|
||||
})
|
||||
@@ -45,57 +47,51 @@ func Test_Url_And_Ssl_Flag(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_Localhost_Url_And_No_Ssl_Flag(t *testing.T) {
|
||||
str, err := BuildString(command.Options{
|
||||
str, err := BuildStringFromOptions(command.Options{
|
||||
Url: "postgres://localhost/database",
|
||||
})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://localhost/database?sslmode=disable", str)
|
||||
|
||||
str, err = BuildString(command.Options{
|
||||
str, err = BuildStringFromOptions(command.Options{
|
||||
Url: "postgres://127.0.0.1/database",
|
||||
})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://127.0.0.1/database?sslmode=disable", str)
|
||||
}
|
||||
|
||||
func Test_Localhost_Url_And_Ssl_Flag(t *testing.T) {
|
||||
str, err := BuildString(command.Options{
|
||||
str, err := BuildStringFromOptions(command.Options{
|
||||
Url: "postgres://localhost/database",
|
||||
Ssl: "require",
|
||||
})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://localhost/database?sslmode=require", str)
|
||||
|
||||
str, err = BuildString(command.Options{
|
||||
str, err = BuildStringFromOptions(command.Options{
|
||||
Url: "postgres://127.0.0.1/database",
|
||||
Ssl: "require",
|
||||
})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://127.0.0.1/database?sslmode=require", str)
|
||||
}
|
||||
|
||||
func Test_Localhost_Url_And_Ssl_Arg(t *testing.T) {
|
||||
str, err := BuildString(command.Options{
|
||||
str, err := BuildStringFromOptions(command.Options{
|
||||
Url: "postgres://localhost/database?sslmode=require",
|
||||
})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://localhost/database?sslmode=require", str)
|
||||
|
||||
str, err = BuildString(command.Options{
|
||||
str, err = BuildStringFromOptions(command.Options{
|
||||
Url: "postgres://127.0.0.1/database?sslmode=require",
|
||||
})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://127.0.0.1/database?sslmode=require", str)
|
||||
}
|
||||
|
||||
func Test_Flag_Args(t *testing.T) {
|
||||
str, err := BuildString(command.Options{
|
||||
str, err := BuildStringFromOptions(command.Options{
|
||||
Host: "host",
|
||||
Port: 5432,
|
||||
User: "user",
|
||||
@@ -116,12 +112,12 @@ func Test_Localhost(t *testing.T) {
|
||||
DbName: "db",
|
||||
}
|
||||
|
||||
str, err := BuildString(opts)
|
||||
str, err := BuildStringFromOptions(opts)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://user:password@localhost:5432/db?sslmode=disable", str)
|
||||
|
||||
opts.Host = "127.0.0.1"
|
||||
str, err = BuildString(opts)
|
||||
str, err = BuildStringFromOptions(opts)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://user:password@127.0.0.1:5432/db?sslmode=disable", str)
|
||||
}
|
||||
@@ -136,7 +132,7 @@ func Test_Localhost_And_Ssl(t *testing.T) {
|
||||
Ssl: "require",
|
||||
}
|
||||
|
||||
str, err := BuildString(opts)
|
||||
str, err := BuildStringFromOptions(opts)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://user:password@localhost:5432/db?sslmode=require", str)
|
||||
}
|
||||
@@ -144,18 +140,19 @@ func Test_Localhost_And_Ssl(t *testing.T) {
|
||||
func Test_No_User(t *testing.T) {
|
||||
opts := command.Options{Host: "host", Port: 5432, DbName: "db"}
|
||||
u, _ := user.Current()
|
||||
str, err := BuildString(opts)
|
||||
str, err := BuildStringFromOptions(opts)
|
||||
userAndPass := url.UserPassword(u.Username, "").String()
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, fmt.Sprintf("postgres://%s@host:5432/db", u.Username), str)
|
||||
assert.Equal(t, fmt.Sprintf("postgres://%s@host:5432/db", userAndPass), str)
|
||||
}
|
||||
|
||||
func Test_Port(t *testing.T) {
|
||||
opts := command.Options{Host: "host", User: "user", Port: 5000, DbName: "db"}
|
||||
str, err := BuildString(opts)
|
||||
str, err := BuildStringFromOptions(opts)
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://user@host:5000/db", str)
|
||||
assert.Equal(t, "postgres://user:@host:5000/db", str)
|
||||
}
|
||||
|
||||
func Test_Blank(t *testing.T) {
|
||||
|
||||
@@ -7,10 +7,9 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Check if the TCP port available on localhost
|
||||
func portAvailable(port int) bool {
|
||||
// IsPortAvailable returns true if there's no listeners on a given port
|
||||
func IsPortAvailable(port int) bool {
|
||||
conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%v", port))
|
||||
|
||||
if err != nil {
|
||||
if strings.Index(err.Error(), "connection refused") > 0 {
|
||||
return true
|
||||
@@ -22,10 +21,10 @@ func portAvailable(port int) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Get available TCP port on localhost by trying available ports in a range
|
||||
func AvailablePort(start int, limit int) (int, error) {
|
||||
// FindAvailablePort returns the first available TCP port in the range
|
||||
func FindAvailablePort(start int, limit int) (int, error) {
|
||||
for i := start; i <= (start + limit); i++ {
|
||||
if portAvailable(i) {
|
||||
if IsPortAvailable(i) {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
|
||||
+12
-12
@@ -10,14 +10,14 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_portAvailable(t *testing.T) {
|
||||
func TestIsPortAvailable(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("FIXME")
|
||||
}
|
||||
|
||||
assert.Equal(t, true, portAvailable(8081))
|
||||
assert.Equal(t, true, IsPortAvailable(30000))
|
||||
|
||||
serv, err := net.Listen("tcp", "127.0.0.1:8081")
|
||||
serv, err := net.Listen("tcp", "127.0.0.1:30000")
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Unable to start test tcp listener:", err)
|
||||
t.Fail()
|
||||
@@ -35,20 +35,20 @@ func Test_portAvailable(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
assert.Equal(t, false, portAvailable(8081))
|
||||
assert.Equal(t, true, portAvailable(8082))
|
||||
assert.Equal(t, false, IsPortAvailable(30000))
|
||||
assert.Equal(t, true, IsPortAvailable(30001))
|
||||
}
|
||||
|
||||
func Test_getAvailablePort(t *testing.T) {
|
||||
func TestFindAvailablePort(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("FIXME")
|
||||
}
|
||||
|
||||
port, err := AvailablePort(8081, 1)
|
||||
port, err := FindAvailablePort(30000, 1)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, 8081, port)
|
||||
assert.Equal(t, 30000, port)
|
||||
|
||||
serv, err := net.Listen("tcp", "127.0.0.1:8081")
|
||||
serv, err := net.Listen("tcp", "127.0.0.1:30000")
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Unable to start test tcp listener:", err)
|
||||
t.Fail()
|
||||
@@ -65,11 +65,11 @@ func Test_getAvailablePort(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
port, err = AvailablePort(8081, 0)
|
||||
port, err = FindAvailablePort(30000, 0)
|
||||
assert.EqualError(t, err, "No available port")
|
||||
assert.Equal(t, -1, port)
|
||||
|
||||
port, err = AvailablePort(8081, 1)
|
||||
port, err = FindAvailablePort(30000, 1)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, 8082, port)
|
||||
assert.Equal(t, 30001, port)
|
||||
}
|
||||
|
||||
+131
-64
File diff suppressed because one or more lines are too long
+48
-35
@@ -1,9 +1,7 @@
|
||||
package statements
|
||||
|
||||
const (
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
PG_DATABASES = `
|
||||
Databases = `
|
||||
SELECT
|
||||
datname
|
||||
FROM
|
||||
@@ -15,7 +13,7 @@ ORDER BY
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
PG_SCHEMAS = `
|
||||
Schemas = `
|
||||
SELECT
|
||||
schema_name
|
||||
FROM
|
||||
@@ -25,7 +23,7 @@ ORDER BY
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
PG_INFO = `
|
||||
Info = `
|
||||
SELECT
|
||||
session_user,
|
||||
current_user,
|
||||
@@ -35,11 +33,22 @@ SELECT
|
||||
inet_client_port(),
|
||||
inet_server_addr(),
|
||||
inet_server_port(),
|
||||
version()`
|
||||
version()`
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
PG_TABLE_INDEXES = `
|
||||
EstimatedTableRowCount = `
|
||||
SELECT
|
||||
reltuples
|
||||
FROM
|
||||
pg_class
|
||||
WHERE
|
||||
oid = ('"' || $1::text || '"."' || $2::text || '"')::regclass
|
||||
`
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TableIndexes = `
|
||||
SELECT
|
||||
indexname, indexdef
|
||||
FROM
|
||||
@@ -50,9 +59,10 @@ WHERE
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
PG_TABLE_CONSTRAINTS = `
|
||||
TableConstraints = `
|
||||
SELECT
|
||||
pg_get_constraintdef(c.oid, true) as condef
|
||||
conname as name,
|
||||
pg_get_constraintdef(c.oid, true) as definition
|
||||
FROM
|
||||
pg_constraint c
|
||||
JOIN
|
||||
@@ -67,23 +77,31 @@ ORDER BY
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
PG_TABLE_INFO = `
|
||||
TableInfo = `
|
||||
SELECT
|
||||
pg_size_pretty(pg_table_size($1)) AS data_size,
|
||||
pg_size_pretty(pg_indexes_size($1)) AS index_size,
|
||||
pg_size_pretty(pg_total_relation_size($1)) AS total_size,
|
||||
(SELECT reltuples FROM pg_class WHERE oid = $1::regclass) AS rows_count`
|
||||
|
||||
TableInfoCockroach = `
|
||||
SELECT
|
||||
'n/a' AS data_size,
|
||||
'n/a' AS index_size,
|
||||
'n/a' AS total_size,
|
||||
'n/a' AS rows_count`
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
PG_TABLE_SCHEMA = `
|
||||
TableSchema = `
|
||||
SELECT
|
||||
column_name,
|
||||
data_type,
|
||||
is_nullable,
|
||||
character_maximum_length,
|
||||
character_set_catalog,
|
||||
column_default
|
||||
column_default,
|
||||
pg_catalog.col_description(('"' || $1::text || '"."' || $2::text || '"')::regclass::oid, ordinal_position) as comment
|
||||
FROM
|
||||
information_schema.columns
|
||||
WHERE
|
||||
@@ -92,7 +110,7 @@ WHERE
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
PG_MATERIALIZED_VIEW_SCHEMA = `
|
||||
MaterializedView = `
|
||||
SELECT
|
||||
attname as column_name,
|
||||
atttypid::regtype AS data_type,
|
||||
@@ -109,26 +127,7 @@ WHERE
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
PG_ACTIVITY = `
|
||||
SELECT
|
||||
datname,
|
||||
query,
|
||||
state,
|
||||
waiting,
|
||||
query_start,
|
||||
state_change,
|
||||
pid,
|
||||
datid,
|
||||
application_name,
|
||||
client_addr
|
||||
FROM
|
||||
pg_stat_activity
|
||||
WHERE
|
||||
state IS NOT NULL`
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
PG_OBJECTS = `
|
||||
Objects = `
|
||||
SELECT
|
||||
n.nspname as "schema",
|
||||
c.relname as "name",
|
||||
@@ -141,7 +140,8 @@ SELECT
|
||||
WHEN 's' THEN 'special'
|
||||
WHEN 'f' THEN 'foreign_table'
|
||||
END as "type",
|
||||
pg_catalog.pg_get_userbyid(c.relowner) as "owner"
|
||||
pg_catalog.pg_get_userbyid(c.relowner) as "owner",
|
||||
pg_catalog.obj_description(c.oid) as "comment"
|
||||
FROM
|
||||
pg_catalog.pg_class c
|
||||
LEFT JOIN
|
||||
@@ -149,6 +149,19 @@ LEFT JOIN
|
||||
WHERE
|
||||
c.relkind IN ('r','v','m','S','s','') AND
|
||||
n.nspname !~ '^pg_toast' AND
|
||||
n.nspname NOT IN ('information_schema', 'pg_catalog')
|
||||
n.nspname NOT IN ('information_schema', 'pg_catalog') AND
|
||||
has_schema_privilege(n.nspname, 'USAGE')
|
||||
ORDER BY 1, 2`
|
||||
)
|
||||
|
||||
var (
|
||||
Activity = map[string]string{
|
||||
"default": "SELECT * FROM pg_stat_activity WHERE datname = current_database()",
|
||||
"9.1": "SELECT datname, current_query, waiting, query_start, procpid as pid, datid, application_name, client_addr FROM pg_stat_activity WHERE datname = current_database()",
|
||||
"9.2": "SELECT datname, query, state, waiting, query_start, state_change, pid, datid, application_name, client_addr FROM pg_stat_activity WHERE datname = current_database()",
|
||||
"9.3": "SELECT datname, query, state, waiting, query_start, state_change, pid, datid, application_name, client_addr FROM pg_stat_activity WHERE datname = current_database()",
|
||||
"9.4": "SELECT datname, query, state, waiting, query_start, state_change, pid, datid, application_name, client_addr FROM pg_stat_activity WHERE datname = current_database()",
|
||||
"9.5": "SELECT datname, query, state, waiting, query_start, state_change, pid, datid, application_name, client_addr FROM pg_stat_activity WHERE datname = current_database()",
|
||||
"9.6": "SELECT datname, query, state, wait_event, wait_event_type, query_start, state_change, pid, datid, application_name, client_addr FROM pg_stat_activity WHERE datname = current_database()",
|
||||
}
|
||||
)
|
||||
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
if grep -q 'go/src/github.com/sosedoff/pgweb' ./pkg/data/bindata.go
|
||||
then
|
||||
echo "=========================================================="
|
||||
echo "ERROR: Bindata contains development references to assets!"
|
||||
echo "Fix with 'make assets' and commit the change."
|
||||
echo "=========================================================="
|
||||
exit 1
|
||||
fi
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Run the fmt on bindata so it does not trigger failure
|
||||
go fmt ./pkg/data/bindata.go > /dev/null
|
||||
|
||||
# Get list of offending files
|
||||
files="$(go fmt ./pkg/...)"
|
||||
|
||||
if [ -n "$files" ]; then
|
||||
echo "Go code is not formatted:"
|
||||
for file in $files; do
|
||||
echo "----> $file"
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
+19
-5
@@ -1,20 +1,34 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Integartion testing with dockerized Postgres servers
|
||||
#
|
||||
# Requires Docker for Mac to run on OSX.
|
||||
# Install: https://docs.docker.com/engine/installation/mac/
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
export PGHOST=${PGHOST:-192.168.99.100}
|
||||
export PGHOST=${PGHOST:-localhost}
|
||||
export PGUSER="postgres"
|
||||
export PGPASSWORD=""
|
||||
export PGDATABASE="booktown"
|
||||
export PGPORT="15432"
|
||||
|
||||
for i in {1..6}
|
||||
# TODO: Enable the 10.x branch when it's supported on Travis.
|
||||
# Local 10.x version is required so that pg_dump can properly work with older versions.
|
||||
# 10.x branch is normally supported.
|
||||
versions="9.1 9.2 9.3 9.4 9.5 9.6"
|
||||
|
||||
for i in $versions
|
||||
do
|
||||
export PGVERSION="9.$i"
|
||||
export PGVERSION="$i"
|
||||
|
||||
echo "------------------------------- BEGIN TEST -------------------------------"
|
||||
echo "Running tests against PostgreSQL v$PGVERSION"
|
||||
docker rm -f postgres || true
|
||||
docker run -p $PGPORT:5432 --name postgres -e POSTGRES_PASSWORD=$PGPASSWORD -d postgres:$PGVERSION
|
||||
sleep 5
|
||||
make test
|
||||
echo "----------"
|
||||
done
|
||||
echo "-------------------------------- END TEST --------------------------------"
|
||||
done
|
||||
|
||||
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
function killproc() {
|
||||
if [[ $(lsof -i tcp:8888) ]]; then
|
||||
lsof -i tcp:8888 | grep pgweb | awk '{print $2}' | xargs kill
|
||||
fi
|
||||
}
|
||||
|
||||
# Nuke the old container if exists.
|
||||
docker rm -f cockroach || true
|
||||
|
||||
# Start cockroach on 26258 so we dont mess with local server.
|
||||
docker run \
|
||||
--name=cockroach \
|
||||
-d \
|
||||
-t \
|
||||
-p 26258:26257 \
|
||||
cockroachdb/cockroach \
|
||||
start --insecure
|
||||
|
||||
sleep 3
|
||||
|
||||
# Load the demo database.
|
||||
docker exec -i cockroach ./cockroach sql --insecure < ./data/roach.sql
|
||||
|
||||
# Find and destroy the existing pgweb process.
|
||||
# Would be great if pgweb had --pid option.
|
||||
killproc
|
||||
|
||||
# Start pgweb and connect to cockroach.
|
||||
make build
|
||||
|
||||
./pgweb \
|
||||
--url=postgres://root@localhost:26258/roach?sslmode=disable \
|
||||
--listen=8888 \
|
||||
--skip-open &
|
||||
|
||||
sleep 1
|
||||
|
||||
# Run smoke tests
|
||||
base="-w \"\n\" -f http://localhost:8888/api"
|
||||
table="product_information"
|
||||
|
||||
curl $base/info
|
||||
curl $base/connection
|
||||
curl $base/schemas
|
||||
curl $base/objects
|
||||
curl $base/query -F query='select * from product_information;'
|
||||
curl $base/tables/$table
|
||||
curl $base/tables/$table/rows
|
||||
curl $base/tables/$table/info
|
||||
curl $base/tables/$table/indexes
|
||||
curl $base/tables/$table/constraints
|
||||
|
||||
# Cleanup
|
||||
killproc
|
||||
+106
-10
@@ -81,18 +81,62 @@
|
||||
line-height: 33px;
|
||||
height: 33px;
|
||||
padding: 0px 8px;
|
||||
padding-left: 12px;
|
||||
color: #555;
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#sidebar .title.main {
|
||||
line-height: 50px;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
font-size: 14px;
|
||||
background: #79589f;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
#sidebar .title.main i.fa-database {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 18px;
|
||||
}
|
||||
|
||||
#sidebar span.current-database,
|
||||
#sidebar input.typeahead {
|
||||
margin-left: 30px;
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
height: 50px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
#sidebar input.typeahead {
|
||||
display: none;
|
||||
background: #79589f;
|
||||
width: 200px;
|
||||
height: 22px;
|
||||
color: #fff;
|
||||
font-weight: normal;
|
||||
outline: none;
|
||||
border: 0px none;
|
||||
}
|
||||
|
||||
#sidebar input.typeahead::-webkit-input-placeholder {
|
||||
color: #d6cce2;
|
||||
}
|
||||
|
||||
#sidebar ul.typeahead {
|
||||
margin-left: 30px;
|
||||
overflow-y: auto;
|
||||
max-height: 300px;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
#sidebar ul.typeahead a {
|
||||
font-size: 13px;
|
||||
padding: 3px 10px;
|
||||
}
|
||||
|
||||
#sidebar div.tables-list {
|
||||
@@ -110,7 +154,7 @@
|
||||
}
|
||||
|
||||
#sidebar div.tables-list .title span.current-database {
|
||||
margin-left: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#sidebar div.tables-list .title:hover span.refresh {
|
||||
@@ -118,8 +162,9 @@
|
||||
}
|
||||
|
||||
#sidebar div.tables-list .title span.refresh {
|
||||
float: right;
|
||||
margin-right: 4px;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 6px;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
display: none;
|
||||
@@ -195,6 +240,16 @@
|
||||
border-bottom: solid 1px #ddd;
|
||||
}
|
||||
|
||||
#input .actions #result-rows-count {
|
||||
font-size: 12px;
|
||||
display: inline-block;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
#input .actions .btn {
|
||||
line-height: 30px;
|
||||
height: 30px;
|
||||
@@ -377,11 +432,21 @@
|
||||
line-height: 24px;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#results th:hover {
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
#results th.active {
|
||||
background: #f3f3f3;
|
||||
background: #f3faff;
|
||||
}
|
||||
|
||||
#results[data-mode="browse"] th:hover {
|
||||
@@ -475,6 +540,7 @@
|
||||
position: fixed;
|
||||
right: 8px;
|
||||
top: 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#edit_connection, #close_connection {
|
||||
@@ -505,14 +571,32 @@
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.connection-settings h1 {
|
||||
.connection-settings .header {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.connection-settings .header h1 {
|
||||
text-align: center;
|
||||
text-shadow: 0px 1px 0px #fff;
|
||||
margin-bottom: 25px;
|
||||
color: #999;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.connection-settings .header .version {
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.connection-settings .header .update {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
padding: 4px;
|
||||
margin: 12px 0px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.connection-settings form {
|
||||
background: #f6f6f6;
|
||||
padding: 25px;
|
||||
@@ -525,6 +609,10 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.connection-settings form .no-left-padding {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.connection-scheme-group {
|
||||
display: none;
|
||||
}
|
||||
@@ -573,6 +661,14 @@
|
||||
line-height: 30px;
|
||||
height: 30px;
|
||||
padding: 0px 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.schema .schema-container .schema-group .schema-group-count {
|
||||
color: #999;
|
||||
display: inline-block;
|
||||
float: right;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.schema .schema-container .schema-group ul {
|
||||
@@ -635,4 +731,4 @@
|
||||
|
||||
.ace_active-line {
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
+71
-22
@@ -13,7 +13,10 @@
|
||||
<script type="text/javascript" src="static/js/ace.js"></script>
|
||||
<script type="text/javascript" src="static/js/ace-pgsql.js"></script>
|
||||
<script type="text/javascript" src="static/js/bootstrap-contextmenu.js"></script>
|
||||
<script type="text/javascript" src="static/js/app.js"></script>
|
||||
<script type="text/javascript" src="static/js/utils.js"></script>
|
||||
<script type="text/javascript" src="static/js/bootstrap3-typeahead.min.js"></script>
|
||||
<script type="text/javascript" src="static/js/app.js"></script>
|
||||
<script type="text/javascript" src="static/js/base64.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="main">
|
||||
@@ -23,7 +26,7 @@
|
||||
<li id="table_structure">Structure</li>
|
||||
<li id="table_indexes">Indexes</li>
|
||||
<li id="table_constraints">Constraints</li>
|
||||
<li id="table_query" class="selected">SQL Query</li>
|
||||
<li id="table_query" class="selected">Query</li>
|
||||
<li id="table_history">History</li>
|
||||
<li id="table_activity">Activity</li>
|
||||
<li id="table_connection">Connection</li>
|
||||
@@ -37,8 +40,9 @@
|
||||
<div id="sidebar">
|
||||
<div class="tables-list">
|
||||
<div class="wrap">
|
||||
<div class="title main">
|
||||
<div class="title main">
|
||||
<i class="fa fa-database"></i> <span class="current-database" id="current_database"></span>
|
||||
<input class="typeahead current_database" id="database_search" type="text" placeholder="Search database">
|
||||
<span class="refresh" id="refresh_tables" title="Refresh tables list"><i class="fa fa-refresh"></i></span>
|
||||
</div>
|
||||
<div id="objects"></div>
|
||||
@@ -65,6 +69,7 @@
|
||||
<input type="button" id="explain" value="Explain Query" class="btn btn-sm btn-default" />
|
||||
<div id="query_progress">Please wait, query is executing...</div>
|
||||
<div class="pull-right">
|
||||
<span id="result-rows-count"></span>
|
||||
<input type="button" id="json" value="JSON" class="btn btn-sm btn-default" />
|
||||
<input type="button" id="csv" value="CSV" class="btn btn-sm btn-default" />
|
||||
<input type="button" id="xml" value="XML" class="btn btn-sm btn-default" />
|
||||
@@ -74,7 +79,10 @@
|
||||
</div>
|
||||
<div id="output">
|
||||
<div class="wrapper">
|
||||
<table id="results" class="table"></table>
|
||||
<table id="results" class="table">
|
||||
<thead id="results_header"></thead>
|
||||
<tbody id="results_body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div id="pagination">
|
||||
@@ -94,7 +102,7 @@
|
||||
<option value="null">IS NULL</option>
|
||||
<option value="not_null">NOT NULL</option>
|
||||
</select>
|
||||
<input type="text" class="form-control" placeholder="Search query" />
|
||||
<input type="text" class="form-control" placeholder="Filter value" id="table_filter_value" />
|
||||
<button class="btn btn-primary btn-sm apply-filters" type="submit">Apply</button>
|
||||
<button class="btn btn-default btn-sm reset-filters"><i class="fa fa-times"></i></button>
|
||||
</form>
|
||||
@@ -112,7 +120,11 @@
|
||||
|
||||
<div id="connection_window">
|
||||
<div class="connection-settings">
|
||||
<h1>pgweb</h1>
|
||||
<div class="header">
|
||||
<h1>pgweb</h1>
|
||||
<div class="version"></div>
|
||||
<div class="update alert alert-warning"></div>
|
||||
</div>
|
||||
|
||||
<form role="form" class="form-horizontal" id="connection_form">
|
||||
<div class="text-center">
|
||||
@@ -129,8 +141,11 @@
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label>Enter server URL scheme</label>
|
||||
<input type="text" class="form-control" id="connection_url" name="url">
|
||||
<p class="help-block">URL format: postgres://user:password@host:port/db?sslmode=mode</p>
|
||||
<input type="text" class="form-control" id="connection_url" name="url" autocomplete="off">
|
||||
<p class="help-block">
|
||||
URL format: postgres://user:password@host:port/db?sslmode=mode<br/>
|
||||
Read more on PostgreSQL <a href="https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING" target="_blank">connection string format</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -145,9 +160,12 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">Host</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="col-sm-7">
|
||||
<input type="text" id="pg_host" class="form-control" />
|
||||
</div>
|
||||
<div class="col-sm-2 no-left-padding">
|
||||
<input type="text" id="pg_port" class="form-control" placeholder="5432" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -172,14 +190,7 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">Port</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" id="pg_port" class="form-control" placeholder="5432" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">SSL</label>
|
||||
<label class="col-sm-3 control-label">SSL Mode</label>
|
||||
<div class="col-sm-9">
|
||||
<select class="form-control" id="connection_ssl">
|
||||
<option value="disable">disable</option>
|
||||
@@ -191,11 +202,16 @@
|
||||
</div>
|
||||
|
||||
<div class="connection-ssh-group">
|
||||
<hr/>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">SSH Host</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="col-sm-7">
|
||||
<input type="text" id="ssh_host" class="form-control" />
|
||||
</div>
|
||||
<div class="col-sm-2 no-left-padding">
|
||||
<input type="text" id="ssh_port" class="form-control" placeholder="22" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -213,9 +229,9 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">SSH Port</label>
|
||||
<label class="col-sm-3 control-label">SSH Key</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" id="ssh_port" class="form-control" placeholder="optional" />
|
||||
<input type="text" id="ssh_key" class="form-control" placeholder="optional" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -233,12 +249,45 @@
|
||||
</div>
|
||||
<div id="tables_context_menu">
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li><a href="#" data-action="copy">Copy Table Name</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="#" data-action="export" data-format="json">Export to JSON</a></li>
|
||||
<li><a href="#" data-action="export" data-format="csv">Export to CSV</a></li>
|
||||
<li><a href="#" data-action="export" data-format="xml">Export to XML</a></li>
|
||||
<li><a href="#" data-action="dump">Export to SQL</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="#" data-action="truncate">Truncate Table</a></li>
|
||||
<li><a href="#" data-action="delete">Delete Table</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="view_context_menu">
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li><a href="#" data-action="copy">Copy View Name</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="#" data-action="export" data-format="json">Export to JSON</a></li>
|
||||
<li><a href="#" data-action="export" data-format="csv">Export to CSV</a></li>
|
||||
<li><a href="#" data-action="export" data-format="xml">Export to XML</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="#" data-action="truncate">Truncate table</a></li>
|
||||
<li><a href="#" data-action="delete">Delete table</a></li>
|
||||
<li><a href="#" data-action="delete">Delete View</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="current_database_context_menu">
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li><a href="#" data-action="export">Export SQL dump</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="results_header_menu">
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li><a href="#" data-action="unique_values" data-counts="false">Unique Values</a></li>
|
||||
<li><a href="#" data-action="unique_values" data-counts="true">Unique Values + Counts</a></li>
|
||||
<li><a href="#" data-action="num_stats">Numeric stats (min/max/avg)</a></li>
|
||||
<li><a href="#" data-action="copy_name">Copy Column Name</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="results_row_menu">
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li><a href="#" data-action="copy_value">Copy Value</a></li>
|
||||
<li><a href="#" data-action="filter_by_value">Filter Rows By Value</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
+458
-77
@@ -17,11 +17,6 @@ var filterOptions = {
|
||||
"not_null": "IS NOT NULL"
|
||||
};
|
||||
|
||||
function guid() {
|
||||
function s4() { return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); }
|
||||
return [s4(), s4(), "-", s4(), "-", s4(), "-", s4(), "-", s4(), s4(), s4()].join("");
|
||||
}
|
||||
|
||||
function getSessionId() {
|
||||
var id = sessionStorage.getItem("session_id");
|
||||
|
||||
@@ -83,6 +78,7 @@ function apiCall(method, path, params, cb) {
|
||||
});
|
||||
}
|
||||
|
||||
function getInfo(cb) { apiCall("get", "/info", {}, cb); }
|
||||
function getObjects(cb) { apiCall("get", "/objects", {}, cb); }
|
||||
function getTables(cb) { apiCall("get", "/tables", {}, cb); }
|
||||
function getTableRows(table, opts, cb) { apiCall("get", "/tables/" + table + "/rows", opts, cb); }
|
||||
@@ -96,7 +92,7 @@ function explainQuery(query, cb) { apiCall("post", "/explain", { quer
|
||||
function disconnect(cb) { apiCall("post", "/disconnect", {}, cb); }
|
||||
|
||||
function encodeQuery(query) {
|
||||
return window.btoa(query).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ".");
|
||||
return Base64.encode(query).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ".");
|
||||
}
|
||||
|
||||
function buildSchemaSection(name, objects) {
|
||||
@@ -123,22 +119,22 @@ function buildSchemaSection(name, objects) {
|
||||
section += "<div class='schema-name'><i class='fa fa-folder-o'></i><i class='fa fa-folder-open-o'></i> " + name + "</div>";
|
||||
section += "<div class='schema-container'>";
|
||||
|
||||
for (group of ["table", "view", "materialized_view", "sequence"]) {
|
||||
if (objects[group].length == 0) continue;
|
||||
|
||||
["table", "view", "materialized_view", "sequence"].forEach(function(group) {
|
||||
group_klass = "";
|
||||
if (name == "public" && group == "table") group_klass = "expanded";
|
||||
|
||||
section += "<div class='schema-group " + group_klass + "'>";
|
||||
section += "<div class='schema-group-title'><i class='fa fa-chevron-right'></i><i class='fa fa-chevron-down'></i> " + titles[group] + " (" + objects[group].length + ")</div>";
|
||||
section += "<ul>"
|
||||
section += "<div class='schema-group-title'><i class='fa fa-chevron-right'></i><i class='fa fa-chevron-down'></i> " + titles[group] + " <span class='schema-group-count'>" + objects[group].length + "</span></div>";
|
||||
section += "<ul data-group='" + group + "'>";
|
||||
|
||||
for (item of objects[group]) {
|
||||
var id = name + "." + item;
|
||||
section += "<li class='schema-" + group + "' data-type='" + group + "' data-id='" + id + "'>" + icons[group] + " " + item + "</li>";
|
||||
if (objects[group]) {
|
||||
objects[group].forEach(function(item) {
|
||||
var id = name + "." + item;
|
||||
section += "<li class='schema-" + group + "' data-type='" + group + "' data-id='" + id + "'>" + icons[group] + " " + item + "</li>";
|
||||
});
|
||||
section += "</ul></div>";
|
||||
}
|
||||
section += "</ul></div>";
|
||||
}
|
||||
});
|
||||
|
||||
section += "</div></div>";
|
||||
|
||||
@@ -149,6 +145,15 @@ function loadSchemas() {
|
||||
$("#objects").html("");
|
||||
|
||||
getObjects(function(data) {
|
||||
if (Object.keys(data).length == 0) {
|
||||
data["public"] = {
|
||||
table: [],
|
||||
view: [],
|
||||
materialized_view: [],
|
||||
sequence: []
|
||||
};
|
||||
}
|
||||
|
||||
for (schema in data) {
|
||||
$(buildSchemaSection(schema, data[schema])).appendTo("#objects");
|
||||
}
|
||||
@@ -181,10 +186,12 @@ function getCurrentObject() {
|
||||
|
||||
function resetTable() {
|
||||
$("#results").
|
||||
attr("data-mode", "").
|
||||
text("").
|
||||
data("mode", "").
|
||||
removeClass("empty").
|
||||
removeClass("no-crop");
|
||||
|
||||
$("#results_header").html("");
|
||||
$("#results_body").html("");
|
||||
}
|
||||
|
||||
function performTableAction(table, action, el) {
|
||||
@@ -209,12 +216,60 @@ function performTableAction(table, action, el) {
|
||||
break;
|
||||
case "export":
|
||||
var format = el.data("format");
|
||||
var filename = table + "." + format;
|
||||
var db = $("#current_database").text();
|
||||
var filename = db + "." + table + "." + format;
|
||||
var query = window.encodeURI("SELECT * FROM " + table);
|
||||
var url = "http://" + window.location.host + "/api/query?format=" + format + "&filename=" + filename + "&query=" + query + "&_session_id=" + getSessionId();
|
||||
var url = window.location.href.split("#")[0] + "api/query?format=" + format + "&filename=" + filename + "&query=" + query + "&_session_id=" + getSessionId();
|
||||
var win = window.open(url, "_blank");
|
||||
win.focus();
|
||||
break;
|
||||
case "dump":
|
||||
var url = window.location.href.split("#")[0] + "api/export?table=" + table + "&_session_id=" + getSessionId();
|
||||
var win = window.open(url, "_blank");
|
||||
win.focus();
|
||||
break;
|
||||
case "copy":
|
||||
copyToClipboard(table.split('.')[1]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function performViewAction(view, action, el) {
|
||||
if (action == "delete") {
|
||||
var message = "Are you sure you want to " + action + " view " + view + " ?";
|
||||
if (!confirm(message)) return;
|
||||
}
|
||||
|
||||
switch(action) {
|
||||
case "delete":
|
||||
executeQuery("DROP VIEW " + view, function(data) {
|
||||
if (data.error) alert(data.error);
|
||||
loadSchemas();
|
||||
resetTable();
|
||||
});
|
||||
break;
|
||||
case "export":
|
||||
var format = el.data("format");
|
||||
var db = $("#current_database").text();
|
||||
var filename = db + "." + view + "." + format;
|
||||
var query = window.encodeURI("SELECT * FROM " + view);
|
||||
var url = window.location.href.split("#")[0] + "api/query?format=" + format + "&filename=" + filename + "&query=" + query + "&_session_id=" + getSessionId();
|
||||
var win = window.open(url, "_blank");
|
||||
win.focus();
|
||||
break;
|
||||
case "copy":
|
||||
copyToClipboard(view.split('.')[1]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function performRowAction(action, value) {
|
||||
if (action == "stop_query") {
|
||||
if (!confirm("Are you sure you want to stop the query?")) return;
|
||||
executeQuery("SELECT pg_cancel_backend(" + value + ");", function(data) {
|
||||
if (data.error) alert(data.error);
|
||||
setTimeout(showActivityPanel, 1000);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,17 +284,22 @@ function sortArrow(direction) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildTable(results, sortColumn, sortOrder) {
|
||||
function buildTable(results, sortColumn, sortOrder, options) {
|
||||
if (!options) options = {};
|
||||
var action = options.action;
|
||||
|
||||
resetTable();
|
||||
|
||||
if (results.error) {
|
||||
$("<tr><td>ERROR: " + results.error + "</tr></tr>").appendTo("#results");
|
||||
$("#results").addClass("empty");
|
||||
$("#results_header").html("");
|
||||
$("#results_body").html("<tr><td>ERROR: " + results.error + "</tr></tr>");
|
||||
return;
|
||||
}
|
||||
|
||||
if (results.rows.length == 0) {
|
||||
$("<tr><td>No records found</tr></tr>").appendTo("#results");
|
||||
$("#results_header").html("");
|
||||
$("#results_body").html("<tr><td>No records found</td></tr>");
|
||||
$("#result-rows-count").html("");
|
||||
$("#results").addClass("empty");
|
||||
return;
|
||||
}
|
||||
@@ -249,25 +309,55 @@ function buildTable(results, sortColumn, sortOrder) {
|
||||
|
||||
results.columns.forEach(function(col) {
|
||||
if (col === sortColumn) {
|
||||
cols += "<th class='active' data='" + col + "'" + "data-sort-order=" + sortOrder + ">" + col + " " + sortArrow(sortOrder) + "</th>";
|
||||
cols += "<th class='table-header-col active' data-name='" + col + "'" + "data-order=" + sortOrder + ">" + col + " " + sortArrow(sortOrder) + "</th>";
|
||||
}
|
||||
else {
|
||||
cols += "<th data='" + col + "'>" + col + "</th>";
|
||||
cols += "<th class='table-header-col' data-name='" + col + "'>" + col + "</th>";
|
||||
}
|
||||
});
|
||||
|
||||
// No header to make the column non-sortable
|
||||
if (action) {
|
||||
cols += "<th></th>";
|
||||
|
||||
// Determine which column contains the data attribute
|
||||
action.dataColumn = results.columns.indexOf(action.data);
|
||||
}
|
||||
|
||||
results.rows.forEach(function(row) {
|
||||
var r = "";
|
||||
for (i in row) { r += "<td><div>" + escapeHtml(row[i]) + "</div></td>"; }
|
||||
|
||||
// Add all actual row data here
|
||||
for (i in row) {
|
||||
r += "<td data-col='" + i + "'><div>" + escapeHtml(row[i]) + "</div></td>";
|
||||
}
|
||||
|
||||
// Add row action button
|
||||
if (action) {
|
||||
r += "<td><a class='btn btn-xs btn-" + action.style + " row-action' data-action='" + action.name + "' data-value='" + row[action.dataColumn] + "' href='#'>" + action.title + "</a></td>";
|
||||
}
|
||||
|
||||
rows += "<tr>" + r + "</tr>";
|
||||
});
|
||||
|
||||
$("<thead>" + cols + "</thead><tbody>" + rows + "</tobdy>").appendTo("#results");
|
||||
$("#results_header").html(cols);
|
||||
$("#results_body").html(rows);
|
||||
|
||||
// Show number of rows rendered on the page
|
||||
$("#result-rows-count").html(results.rows.length + " rows");
|
||||
}
|
||||
|
||||
function setCurrentTab(id) {
|
||||
// Pagination should only be visible on rows tab
|
||||
if (id != "table_content") {
|
||||
$("#body").removeClass("with-pagination");
|
||||
}
|
||||
|
||||
$("#nav ul li.selected").removeClass("selected");
|
||||
$("#" + id).addClass("selected");
|
||||
|
||||
// Persist tab selection into the session storage
|
||||
sessionStorage.setItem("tab", id);
|
||||
}
|
||||
|
||||
function showQueryHistory() {
|
||||
@@ -406,16 +496,30 @@ function showTableContent(sortColumn, sortOrder) {
|
||||
}
|
||||
|
||||
getTableRows(name, opts, function(data) {
|
||||
$("#results").attr("data-mode", "browse");
|
||||
$("#input").hide();
|
||||
$("#body").prop("class", "with-pagination");
|
||||
|
||||
buildTable(data, sortColumn, sortOrder);
|
||||
setCurrentTab("table_content");
|
||||
updatePaginator(data.pagination);
|
||||
|
||||
$("#results").data("mode", "browse").data("table", name);
|
||||
});
|
||||
}
|
||||
|
||||
function showPaginatedTableContent() {
|
||||
var activeColumn = $("#results th.active");
|
||||
var sortColumn = null;
|
||||
var sortOrder = null;
|
||||
|
||||
if (activeColumn.length) {
|
||||
sortColumn = activeColumn.data("name");
|
||||
sortOrder = activeColumn.data("order");
|
||||
}
|
||||
|
||||
showTableContent(sortColumn, sortOrder);
|
||||
}
|
||||
|
||||
function showTableStructure() {
|
||||
var name = getCurrentObject().name;
|
||||
|
||||
@@ -425,12 +529,10 @@ function showTableStructure() {
|
||||
}
|
||||
|
||||
setCurrentTab("table_structure");
|
||||
|
||||
|
||||
$("#input").hide();
|
||||
$("#body").prop("class", "full");
|
||||
|
||||
console.log(getCurrentObject());
|
||||
|
||||
getTableStructure(name, { type: getCurrentObject().type }, function(data) {
|
||||
buildTable(data);
|
||||
$("#results").addClass("no-crop");
|
||||
@@ -438,6 +540,10 @@ function showTableStructure() {
|
||||
}
|
||||
|
||||
function showQueryPanel() {
|
||||
if (!$("#table_query").hasClass("selected")) {
|
||||
resetTable();
|
||||
}
|
||||
|
||||
setCurrentTab("table_query");
|
||||
editor.focus();
|
||||
|
||||
@@ -466,10 +572,18 @@ function showConnectionPanel() {
|
||||
}
|
||||
|
||||
function showActivityPanel() {
|
||||
setCurrentTab("table_activity");
|
||||
var options = {
|
||||
action: {
|
||||
name: "stop_query",
|
||||
title: "stop",
|
||||
data: "pid",
|
||||
style: "danger"
|
||||
}
|
||||
}
|
||||
|
||||
setCurrentTab("table_activity");
|
||||
apiCall("get", "/activity", {}, function(data) {
|
||||
buildTable(data);
|
||||
buildTable(data, null, null, options);
|
||||
$("#input").hide();
|
||||
$("#body").addClass("full");
|
||||
});
|
||||
@@ -496,15 +610,14 @@ function runQuery() {
|
||||
$("#query_progress").hide();
|
||||
$("#input").show();
|
||||
$("#body").removeClass("full");
|
||||
$("#results").data("mode", "query");
|
||||
|
||||
if (query.toLowerCase().indexOf("explain") != -1) {
|
||||
$("#results").addClass("no-crop");
|
||||
}
|
||||
|
||||
var re = /(create|drop)\s/i;
|
||||
|
||||
// Reload objects list if anything was created/deleted
|
||||
if (query.match(re)) {
|
||||
if (query.match(/(create|drop)\s/i)) {
|
||||
loadSchemas();
|
||||
}
|
||||
});
|
||||
@@ -516,7 +629,7 @@ function runExplain() {
|
||||
$("#run, #explain, #csv, #json, #xml").prop("disabled", true);
|
||||
$("#query_progress").show();
|
||||
|
||||
var query = $.trim(editor.getValue());
|
||||
var query = $.trim(editor.getSelectedText() || editor.getValue());
|
||||
|
||||
if (query.length == 0) {
|
||||
$("#run, #explain, #csv, #json, #xml").prop("disabled", false);
|
||||
@@ -536,19 +649,49 @@ function runExplain() {
|
||||
}
|
||||
|
||||
function exportTo(format) {
|
||||
var query = $.trim(editor.getValue());
|
||||
var query = $.trim(editor.getSelectedText() || editor.getValue());
|
||||
|
||||
if (query.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var url = "http://" + window.location.host + "/api/query?format=" + format + "&query=" + encodeQuery(query) + "&_session_id=" + getSessionId();
|
||||
var url = window.location.href.split("#")[0] + "api/query?format=" + format + "&query=" + encodeQuery(query) + "&_session_id=" + getSessionId();
|
||||
var win = window.open(url, '_blank');
|
||||
|
||||
setCurrentTab("table_query");
|
||||
win.focus();
|
||||
}
|
||||
|
||||
// Fetch all unique values for the selected column in the table
|
||||
function showUniqueColumnsValues(table, column, showCounts) {
|
||||
var query = 'SELECT DISTINCT "' + column + '" FROM ' + table;
|
||||
|
||||
// Display results ordered by counts.
|
||||
// This could be slow on large sets without an index.
|
||||
if (showCounts) {
|
||||
query = 'SELECT DISTINCT "' + column + '", COUNT(1) AS total_count FROM ' + table + ' GROUP BY "' + column + '" ORDER BY total_count DESC';
|
||||
}
|
||||
|
||||
executeQuery(query, function(data) {
|
||||
$("#input").hide();
|
||||
$("#body").prop("class", "full");
|
||||
$("#results").data("mode", "query");
|
||||
buildTable(data);
|
||||
});
|
||||
}
|
||||
|
||||
// Show numeric stats on the field
|
||||
function showFieldNumStats(table, column) {
|
||||
var query = 'SELECT count(1), min(' + column + '), max(' + column + '), avg(' + column + ') FROM ' + table;
|
||||
|
||||
executeQuery(query, function(data) {
|
||||
$("#input").hide();
|
||||
$("#body").prop("class", "full");
|
||||
$("#results").data("mode", "query");
|
||||
buildTable(data);
|
||||
});
|
||||
}
|
||||
|
||||
function buildTableFilters(name, type) {
|
||||
getTableStructure(name, { type: type }, function(data) {
|
||||
if (data.rows.length == 0) {
|
||||
@@ -560,7 +703,9 @@ function buildTableFilters(name, type) {
|
||||
|
||||
$("#pagination select.column").html("<option value='' selected>Select column</option>");
|
||||
|
||||
for (row of data.rows) {
|
||||
for (var i = 0; i < data.rows.length; i++) {
|
||||
var row = data.rows[i];
|
||||
|
||||
var el = $("<option/>").attr("value", row[0]).text(row[0]);
|
||||
$("#pagination select.column").append(el);
|
||||
}
|
||||
@@ -626,7 +771,36 @@ function addShortcutTooltips() {
|
||||
}
|
||||
}
|
||||
|
||||
// Get the latest release from Github API
|
||||
function getLatestReleaseInfo(current) {
|
||||
try {
|
||||
$.get("https://api.github.com/repos/sosedoff/pgweb/releases/latest", function(release) {
|
||||
if (release.name != current.version) {
|
||||
var message = "Update available. Check out " + release.tag_name + " on <a target='_blank' href='" + release.html_url + "'>Github</a>";
|
||||
$(".connection-settings .update").html(message).fadeIn();
|
||||
}
|
||||
});
|
||||
}
|
||||
catch(error) {
|
||||
console.log("Cant get last release from github:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function showConnectionSettings() {
|
||||
// Fetch server info
|
||||
getInfo(function(data) {
|
||||
if (data.error) return;
|
||||
if (!data.version) return;
|
||||
|
||||
// Show the current postgres version
|
||||
$(".connection-settings .version").text("v" + data.version).show();
|
||||
|
||||
// Check for updates if running the actual release from Github
|
||||
if (data.git_sha == "") {
|
||||
getLatestReleaseInfo(data);
|
||||
}
|
||||
});
|
||||
|
||||
getBookmarks(function(data) {
|
||||
// Do not add any bookmarks if we've got an error
|
||||
if (data.error) {
|
||||
@@ -687,18 +861,162 @@ function getConnectionString() {
|
||||
return url;
|
||||
}
|
||||
|
||||
function bindContextMenus() {
|
||||
$(".schema-group ul").each(function(id, el) {
|
||||
$(el).contextmenu({
|
||||
target: "#tables_context_menu",
|
||||
scopes: "li.schema-table",
|
||||
onItem: function(context, e) {
|
||||
var el = $(e.target);
|
||||
var table = $(context[0]).data("id");
|
||||
var action = el.data("action");
|
||||
performTableAction(table, action, el);
|
||||
// Add a context menu to the results table header columns
|
||||
function bindTableHeaderMenu() {
|
||||
$("#results_header").contextmenu({
|
||||
scopes: "th",
|
||||
target: "#results_header_menu",
|
||||
before: function(e, element, target) {
|
||||
// Enable menu for browsing table rows view only.
|
||||
if ($("#results").data("mode") != "browse") {
|
||||
e.preventDefault();
|
||||
this.closemenu();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
},
|
||||
onItem: function(context, e) {
|
||||
var menuItem = $(e.target);
|
||||
|
||||
switch(menuItem.data("action")) {
|
||||
case "copy_name":
|
||||
copyToClipboard($(context).data("name"));
|
||||
break;
|
||||
|
||||
case "unique_values":
|
||||
showUniqueColumnsValues(
|
||||
$("#results").data("table"), // table name
|
||||
$(context).data("name"), // column name
|
||||
menuItem.data("counts") // display counts
|
||||
);
|
||||
break;
|
||||
|
||||
case "num_stats":
|
||||
showFieldNumStats(
|
||||
$("#results").data("table"), // table name
|
||||
$(context).data("name") // column name
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$("#results_body").contextmenu({
|
||||
scopes: "td",
|
||||
target: "#results_row_menu",
|
||||
before: function(e, element, target) {
|
||||
var isEmpty = $("#results").hasClass("empty");
|
||||
var isBrowsing = $("#results").data("mode") == "browse";
|
||||
|
||||
if (isEmpty || !isBrowsing) {
|
||||
e.preventDefault();
|
||||
this.closemenu();
|
||||
return false;
|
||||
}
|
||||
},
|
||||
onItem: function(context, e) {
|
||||
var menuItem = $(e.target);
|
||||
|
||||
switch(menuItem.data("action")) {
|
||||
case "copy_value":
|
||||
copyToClipboard($(context).text());
|
||||
break;
|
||||
case "filter_by_value":
|
||||
var colIdx = $(context).data("col");
|
||||
var colValue = $(context).text();
|
||||
var colName = $("#results_header th").eq(colIdx).data("name");
|
||||
|
||||
$("select.column").val(colName);
|
||||
$("select.filter").val("equal");
|
||||
$("#table_filter_value").val(colValue);
|
||||
$("#rows_filter").submit();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function bindCurrentDatabaseMenu() {
|
||||
$("#current_database").contextmenu({
|
||||
target: "#current_database_context_menu",
|
||||
onItem: function(context, e) {
|
||||
var menuItem = $(e.target);
|
||||
|
||||
switch(menuItem.data("action")) {
|
||||
case "export":
|
||||
var url = window.location.href.split("#")[0] + "api/export?_session_id=" + getSessionId();
|
||||
var win = window.open(url, "_blank");
|
||||
win.focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getQuotedSchemaTableName(table) {
|
||||
if (typeof table === "string" && table.indexOf(".") > -1) {
|
||||
var schemaTableComponents = table.split(".");
|
||||
return ['"', schemaTableComponents[0], '"."', schemaTableComponents[1], '"'].join('');
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
function bindContextMenus() {
|
||||
bindTableHeaderMenu();
|
||||
bindCurrentDatabaseMenu();
|
||||
|
||||
$(".schema-group ul").each(function(id, el) {
|
||||
var group = $(el).data("group");
|
||||
|
||||
if (group == "table") {
|
||||
$(el).contextmenu({
|
||||
target: "#tables_context_menu",
|
||||
scopes: "li.schema-table",
|
||||
onItem: function(context, e) {
|
||||
var el = $(e.target);
|
||||
var table = getQuotedSchemaTableName($(context[0]).data("id"));
|
||||
var action = el.data("action");
|
||||
performTableAction(table, action, el);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (group == "view") {
|
||||
$(el).contextmenu({
|
||||
target: "#view_context_menu",
|
||||
scopes: "li.schema-view",
|
||||
onItem: function(context, e) {
|
||||
var el = $(e.target);
|
||||
var table = getQuotedSchemaTableName($(context[0]).data("id"));
|
||||
var action = el.data("action");
|
||||
performViewAction(table, action, el);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleDatabaseSearch() {
|
||||
$("#current_database").toggle();
|
||||
$("#database_search").toggle();
|
||||
}
|
||||
|
||||
function enableDatabaseSearch(data) {
|
||||
var input = $("#database_search");
|
||||
|
||||
input.typeahead("destroy");
|
||||
|
||||
input.typeahead({
|
||||
source: data,
|
||||
minLength: 0,
|
||||
items: "all",
|
||||
autoSelect: false,
|
||||
fitToElement: true
|
||||
});
|
||||
|
||||
input.typeahead("lookup").focus();
|
||||
|
||||
input.on("focusout", function(e){
|
||||
toggleDatabaseSearch();
|
||||
input.off("focusout");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -732,7 +1050,7 @@ $(document).ready(function() {
|
||||
exportTo("xml");
|
||||
});
|
||||
|
||||
$("#results").on("click", "tr", function() {
|
||||
$("#results").on("click", "tr", function(e) {
|
||||
$("#results tr.selected").removeClass();
|
||||
$(this).addClass("selected");
|
||||
});
|
||||
@@ -757,25 +1075,42 @@ $(document).ready(function() {
|
||||
$(".filters select, .filters input").val("");
|
||||
|
||||
showTableInfo();
|
||||
showTableContent();
|
||||
|
||||
switch(sessionStorage.getItem("tab")) {
|
||||
case "table_content":
|
||||
showTableContent();
|
||||
break;
|
||||
case "table_structure":
|
||||
showTableStructure();
|
||||
break;
|
||||
case "table_constraints":
|
||||
showTableConstraints();
|
||||
break;
|
||||
case "table_indexes":
|
||||
showTableIndexes();
|
||||
break;
|
||||
default:
|
||||
showTableContent();
|
||||
}
|
||||
});
|
||||
|
||||
$("#results").on("click", "a.row-action", function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var action = $(this).data("action");
|
||||
var value = $(this).data("value");
|
||||
|
||||
performRowAction(action, value);
|
||||
})
|
||||
|
||||
$("#results").on("click", "th", function(e) {
|
||||
var sortColumn = this.attributes['data'].value;
|
||||
var contentTab = $('#table_content').hasClass('selected');
|
||||
if (!$("#table_content").hasClass("selected")) return;
|
||||
|
||||
if (!contentTab) {
|
||||
return;
|
||||
}
|
||||
var sortColumn = $(this).data("name");
|
||||
var sortOrder = $(this).data("order") === "ASC" ? "DESC" : "ASC";
|
||||
|
||||
if (this.dataset.sortOrder === "ASC") {
|
||||
this.dataset.sortOrder = "DESC"
|
||||
}
|
||||
else {
|
||||
this.dataset.sortOrder = "ASC"
|
||||
}
|
||||
|
||||
showTableContent(sortColumn, this.dataset.sortOrder);
|
||||
$(this).data("order", sortOrder);
|
||||
showTableContent(sortColumn, sortOrder);
|
||||
});
|
||||
|
||||
$("#results").on("dblclick", "td > div", function() {
|
||||
@@ -844,13 +1179,21 @@ $(document).ready(function() {
|
||||
showTableContent();
|
||||
});
|
||||
|
||||
// Automatically prefill the filter if it's not set yet
|
||||
$("select.column").on("change", function() {
|
||||
if ($("select.filter").val() == "") {
|
||||
$("select.filter").val("equal");
|
||||
$("#table_filter_value").focus();
|
||||
}
|
||||
});
|
||||
|
||||
$("#pagination .next-page").on("click", function() {
|
||||
var current = $(".current-page").data("page");
|
||||
var total = $(".current-page").data("pages");
|
||||
|
||||
if (total > current) {
|
||||
$(".current-page").data("page", current + 1);
|
||||
showTableContent();
|
||||
showPaginatedTableContent();
|
||||
|
||||
if (current + 1 == total) {
|
||||
$(this).prop("disabled", "disabled");
|
||||
@@ -868,7 +1211,7 @@ $(document).ready(function() {
|
||||
if (current > 1) {
|
||||
$(".current-page").data("page", current - 1);
|
||||
$(".next-page").prop("disabled", "");
|
||||
showTableContent();
|
||||
showPaginatedTableContent();
|
||||
}
|
||||
|
||||
if (current == 1) {
|
||||
@@ -876,6 +1219,26 @@ $(document).ready(function() {
|
||||
}
|
||||
});
|
||||
|
||||
$("#current_database").on("click", function(e) {
|
||||
apiCall("get", "/databases", {}, function(resp) {
|
||||
toggleDatabaseSearch();
|
||||
enableDatabaseSearch(resp);
|
||||
});
|
||||
});
|
||||
|
||||
$("#database_search").change(function(e) {
|
||||
var current = $("#database_search").typeahead("getActive");
|
||||
if (current && current == $("#database_search").val()) {
|
||||
apiCall("post", "/switchdb", { db: current }, function(resp) {
|
||||
if (resp.error) {
|
||||
alert(resp.error);
|
||||
return;
|
||||
};
|
||||
window.location.reload();
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
$("#edit_connection").on("click", function() {
|
||||
if (connected) {
|
||||
$("#close_connection_window").show();
|
||||
@@ -885,8 +1248,11 @@ $(document).ready(function() {
|
||||
});
|
||||
|
||||
$("#close_connection").on("click", function() {
|
||||
if (!confirm("Are you sure you want to disconnect?")) return;
|
||||
|
||||
disconnect(function() {
|
||||
showConnectionSettings();
|
||||
resetTable();
|
||||
$("#close_connection_window").hide();
|
||||
});
|
||||
});
|
||||
@@ -934,15 +1300,12 @@ $(document).ready(function() {
|
||||
|
||||
$("#connection_bookmarks").on("change", function(e) {
|
||||
var name = $.trim($(this).val());
|
||||
if (name == "") return;
|
||||
|
||||
if (name == "") {
|
||||
return;
|
||||
}
|
||||
|
||||
item = bookmarks[name];
|
||||
var item = bookmarks[name];
|
||||
|
||||
// Check if bookmark only has url set
|
||||
if (item.url != "") {
|
||||
if (item.url && item.url != "") {
|
||||
$("#connection_url").val(item.url);
|
||||
$("#connection_scheme").click();
|
||||
return;
|
||||
@@ -955,12 +1318,13 @@ $(document).ready(function() {
|
||||
$("#pg_password").val(item.password);
|
||||
$("#pg_db").val(item.database);
|
||||
$("#connection_ssl").val(item.ssl);
|
||||
|
||||
if (Object.keys(item.ssh).length > 0) {
|
||||
|
||||
if (item.ssh && Object.keys(item.ssh).length > 0) {
|
||||
$("#ssh_host").val(item.ssh.host);
|
||||
$("#ssh_port").val(item.ssh.port);
|
||||
$("#ssh_user").val(item.ssh.user);
|
||||
$("#ssh_password").val(item.ssh.password);
|
||||
$("#ssh_key").val(item.ssh.key);
|
||||
$("#connection_ssh").click();
|
||||
}
|
||||
else {
|
||||
@@ -968,7 +1332,9 @@ $(document).ready(function() {
|
||||
$("#ssh_port").val("");
|
||||
$("#ssh_user").val("");
|
||||
$("#ssh_password").val("");
|
||||
$("#ssh_key").val("");
|
||||
$(".connection-ssh-group").hide();
|
||||
$("#connection_standard").click();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -990,6 +1356,7 @@ $(document).ready(function() {
|
||||
params["ssh_port"] = $("#ssh_port").val();
|
||||
params["ssh_user"] = $("#ssh_user").val();
|
||||
params["ssh_password"] = $("#ssh_password").val();
|
||||
params["ssh_key"] = $("#ssh_key").val();
|
||||
}
|
||||
|
||||
$("#connection_error").hide();
|
||||
@@ -1016,10 +1383,20 @@ $(document).ready(function() {
|
||||
initEditor();
|
||||
addShortcutTooltips();
|
||||
|
||||
// Set session from the url
|
||||
var reqUrl = new URL(window.location);
|
||||
var sessionId = reqUrl.searchParams.get("session");
|
||||
|
||||
if (sessionId && sessionId != "") {
|
||||
sessionStorage.setItem("session_id", sessionId);
|
||||
window.history.pushState({}, document.title, window.location.pathname);
|
||||
}
|
||||
|
||||
apiCall("get", "/connection", {}, function(resp) {
|
||||
if (resp.error) {
|
||||
connected = false;
|
||||
showConnectionSettings();
|
||||
$(".connection-actions").show();
|
||||
}
|
||||
else {
|
||||
connected = true;
|
||||
@@ -1027,6 +1404,10 @@ $(document).ready(function() {
|
||||
|
||||
$("#current_database").text(resp.current_database);
|
||||
$("#main").show();
|
||||
|
||||
if (!resp.session_lock) {
|
||||
$(".connection-actions").show();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
*
|
||||
* Base64 encode / decode
|
||||
* http://www.webtoolkit.info/
|
||||
*
|
||||
**/
|
||||
var Base64 = {
|
||||
// private property
|
||||
_keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
|
||||
// public method for encoding
|
||||
encode : function (input) {
|
||||
var output = "";
|
||||
var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
|
||||
var i = 0;
|
||||
input = Base64._utf8_encode(input);
|
||||
while (i < input.length) {
|
||||
chr1 = input.charCodeAt(i++);
|
||||
chr2 = input.charCodeAt(i++);
|
||||
chr3 = input.charCodeAt(i++);
|
||||
enc1 = chr1 >> 2;
|
||||
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
|
||||
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
|
||||
enc4 = chr3 & 63;
|
||||
if (isNaN(chr2)) {
|
||||
enc3 = enc4 = 64;
|
||||
} else if (isNaN(chr3)) {
|
||||
enc4 = 64;
|
||||
}
|
||||
output = output +
|
||||
this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
|
||||
this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
|
||||
}
|
||||
return output;
|
||||
},
|
||||
// public method for decoding
|
||||
decode : function (input) {
|
||||
var output = "";
|
||||
var chr1, chr2, chr3;
|
||||
var enc1, enc2, enc3, enc4;
|
||||
var i = 0;
|
||||
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
|
||||
while (i < input.length) {
|
||||
enc1 = this._keyStr.indexOf(input.charAt(i++));
|
||||
enc2 = this._keyStr.indexOf(input.charAt(i++));
|
||||
enc3 = this._keyStr.indexOf(input.charAt(i++));
|
||||
enc4 = this._keyStr.indexOf(input.charAt(i++));
|
||||
chr1 = (enc1 << 2) | (enc2 >> 4);
|
||||
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
|
||||
chr3 = ((enc3 & 3) << 6) | enc4;
|
||||
output = output + String.fromCharCode(chr1);
|
||||
if (enc3 != 64) {
|
||||
output = output + String.fromCharCode(chr2);
|
||||
}
|
||||
if (enc4 != 64) {
|
||||
output = output + String.fromCharCode(chr3);
|
||||
}
|
||||
}
|
||||
output = Base64._utf8_decode(output);
|
||||
return output;
|
||||
},
|
||||
// private method for UTF-8 encoding
|
||||
_utf8_encode : function (string) {
|
||||
string = string.replace(/\r\n/g,"\n");
|
||||
var utftext = "";
|
||||
for (var n = 0; n < string.length; n++) {
|
||||
var c = string.charCodeAt(n);
|
||||
if (c < 128) {
|
||||
utftext += String.fromCharCode(c);
|
||||
}
|
||||
else if((c > 127) && (c < 2048)) {
|
||||
utftext += String.fromCharCode((c >> 6) | 192);
|
||||
utftext += String.fromCharCode((c & 63) | 128);
|
||||
}
|
||||
else {
|
||||
utftext += String.fromCharCode((c >> 12) | 224);
|
||||
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
|
||||
utftext += String.fromCharCode((c & 63) | 128);
|
||||
}
|
||||
}
|
||||
return utftext;
|
||||
},
|
||||
// private method for UTF-8 decoding
|
||||
_utf8_decode : function (utftext) {
|
||||
var string = "";
|
||||
var i = 0;
|
||||
var c = c1 = c2 = 0;
|
||||
while ( i < utftext.length ) {
|
||||
c = utftext.charCodeAt(i);
|
||||
if (c < 128) {
|
||||
string += String.fromCharCode(c);
|
||||
i++;
|
||||
}
|
||||
else if((c > 191) && (c < 224)) {
|
||||
c2 = utftext.charCodeAt(i+1);
|
||||
string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
|
||||
i += 2;
|
||||
}
|
||||
else {
|
||||
c2 = utftext.charCodeAt(i+1);
|
||||
c3 = utftext.charCodeAt(i+2);
|
||||
string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
|
||||
i += 3;
|
||||
}
|
||||
}
|
||||
return string;
|
||||
}
|
||||
}
|
||||
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,28 @@
|
||||
if (!Array.prototype.forEach) {
|
||||
// Simplified iterator for browsers without forEach support
|
||||
Array.prototype.forEach = function(cb) {
|
||||
if (typeof this.length != 'number') return;
|
||||
if (typeof callback != 'function') return;
|
||||
|
||||
for (var i = 0; i < this.length; i++) cb(this[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function copyToClipboard(text) {
|
||||
const element = document.createElement("textarea");
|
||||
element.style.display = "none;"
|
||||
element.value = text;
|
||||
|
||||
document.body.appendChild(element);
|
||||
element.focus();
|
||||
element.setSelectionRange(0, element.value.length);
|
||||
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
|
||||
function guid() {
|
||||
function s4() { return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); }
|
||||
return [s4(), s4(), "-", s4(), "-", s4(), "-", s4(), "-", s4(), s4(), s4()].join("");
|
||||
}
|
||||
|
||||
+4
-1
@@ -2,6 +2,10 @@ language: go
|
||||
go:
|
||||
- 1.1
|
||||
- 1.2
|
||||
- 1.3
|
||||
- 1.4
|
||||
- 1.5
|
||||
- 1.6
|
||||
- tip
|
||||
install:
|
||||
- go install ./...
|
||||
@@ -9,4 +13,3 @@ install:
|
||||
script:
|
||||
- export PATH="$PATH:$HOME/gopath/bin"
|
||||
- make test
|
||||
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
Compatible with TOML version
|
||||
[v0.2.0](https://github.com/mojombo/toml/blob/master/versions/toml-v0.2.0.md)
|
||||
[v0.4.0](https://github.com/toml-lang/toml/blob/v0.4.0/versions/en/toml-v0.4.0.md)
|
||||
|
||||
|
||||
+8
-10
@@ -1,17 +1,17 @@
|
||||
## TOML parser and encoder for Go with reflection
|
||||
|
||||
TOML stands for Tom's Obvious, Minimal Language. This Go package provides a
|
||||
reflection interface similar to Go's standard library `json` and `xml`
|
||||
reflection interface similar to Go's standard library `json` and `xml`
|
||||
packages. This package also supports the `encoding.TextUnmarshaler` and
|
||||
`encoding.TextMarshaler` interfaces so that you can define custom data
|
||||
`encoding.TextMarshaler` interfaces so that you can define custom data
|
||||
representations. (There is an example of this below.)
|
||||
|
||||
Spec: https://github.com/mojombo/toml
|
||||
Spec: https://github.com/toml-lang/toml
|
||||
|
||||
Compatible with TOML version
|
||||
[v0.2.0](https://github.com/mojombo/toml/blob/master/versions/toml-v0.2.0.md)
|
||||
[v0.4.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md)
|
||||
|
||||
Documentation: http://godoc.org/github.com/BurntSushi/toml
|
||||
Documentation: https://godoc.org/github.com/BurntSushi/toml
|
||||
|
||||
Installation:
|
||||
|
||||
@@ -26,8 +26,7 @@ go get github.com/BurntSushi/toml/cmd/tomlv
|
||||
tomlv some-toml-file.toml
|
||||
```
|
||||
|
||||
[](https://travis-ci.org/BurntSushi/toml)
|
||||
|
||||
[](https://travis-ci.org/BurntSushi/toml) [](https://godoc.org/github.com/BurntSushi/toml)
|
||||
|
||||
### Testing
|
||||
|
||||
@@ -87,7 +86,7 @@ type TOML struct {
|
||||
|
||||
### Using the `encoding.TextUnmarshaler` interface
|
||||
|
||||
Here's an example that automatically parses duration strings into
|
||||
Here's an example that automatically parses duration strings into
|
||||
`time.Duration` values:
|
||||
|
||||
```toml
|
||||
@@ -120,7 +119,7 @@ for _, s := range favorites.Song {
|
||||
}
|
||||
```
|
||||
|
||||
And you'll also need a `duration` type that satisfies the
|
||||
And you'll also need a `duration` type that satisfies the
|
||||
`encoding.TextUnmarshaler` interface:
|
||||
|
||||
```go
|
||||
@@ -217,4 +216,3 @@ Note that a case insensitive match will be tried if an exact match can't be
|
||||
found.
|
||||
|
||||
A working example of the above can be found in `_examples/example.{go,toml}`.
|
||||
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
Version 2, December 2004
|
||||
|
||||
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim or modified
|
||||
copies of this license document, and changing it is allowed as long
|
||||
as the name is changed.
|
||||
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. You just DO WHAT THE FUCK YOU WANT TO.
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
Version 2, December 2004
|
||||
|
||||
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim or modified
|
||||
copies of this license document, and changing it is allowed as long
|
||||
as the name is changed.
|
||||
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. You just DO WHAT THE FUCK YOU WANT TO.
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
Version 2, December 2004
|
||||
|
||||
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim or modified
|
||||
copies of this license document, and changing it is allowed as long
|
||||
as the name is changed.
|
||||
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. You just DO WHAT THE FUCK YOU WANT TO.
|
||||
|
||||
+42
-25
@@ -10,7 +10,9 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var e = fmt.Errorf
|
||||
func e(format string, args ...interface{}) error {
|
||||
return fmt.Errorf("toml: "+format, args...)
|
||||
}
|
||||
|
||||
// Unmarshaler is the interface implemented by objects that can unmarshal a
|
||||
// TOML description of themselves.
|
||||
@@ -103,6 +105,13 @@ func (md *MetaData) PrimitiveDecode(primValue Primitive, v interface{}) error {
|
||||
// This decoder will not handle cyclic types. If a cyclic type is passed,
|
||||
// `Decode` will not terminate.
|
||||
func Decode(data string, v interface{}) (MetaData, error) {
|
||||
rv := reflect.ValueOf(v)
|
||||
if rv.Kind() != reflect.Ptr {
|
||||
return MetaData{}, e("Decode of non-pointer %s", reflect.TypeOf(v))
|
||||
}
|
||||
if rv.IsNil() {
|
||||
return MetaData{}, e("Decode of nil %s", reflect.TypeOf(v))
|
||||
}
|
||||
p, err := parse(data)
|
||||
if err != nil {
|
||||
return MetaData{}, err
|
||||
@@ -111,7 +120,7 @@ func Decode(data string, v interface{}) (MetaData, error) {
|
||||
p.mapping, p.types, p.ordered,
|
||||
make(map[string]bool, len(p.ordered)), nil,
|
||||
}
|
||||
return md, md.unify(p.mapping, rvalue(v))
|
||||
return md, md.unify(p.mapping, indirect(rv))
|
||||
}
|
||||
|
||||
// DecodeFile is just like Decode, except it will automatically read the
|
||||
@@ -211,7 +220,7 @@ func (md *MetaData) unify(data interface{}, rv reflect.Value) error {
|
||||
case reflect.Interface:
|
||||
// we only support empty interfaces.
|
||||
if rv.NumMethod() > 0 {
|
||||
return e("Unsupported type '%s'.", rv.Kind())
|
||||
return e("unsupported type %s", rv.Type())
|
||||
}
|
||||
return md.unifyAnything(data, rv)
|
||||
case reflect.Float32:
|
||||
@@ -219,13 +228,17 @@ func (md *MetaData) unify(data interface{}, rv reflect.Value) error {
|
||||
case reflect.Float64:
|
||||
return md.unifyFloat64(data, rv)
|
||||
}
|
||||
return e("Unsupported type '%s'.", rv.Kind())
|
||||
return e("unsupported type %s", rv.Kind())
|
||||
}
|
||||
|
||||
func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error {
|
||||
tmap, ok := mapping.(map[string]interface{})
|
||||
if !ok {
|
||||
return mismatch(rv, "map", mapping)
|
||||
if mapping == nil {
|
||||
return nil
|
||||
}
|
||||
return e("type mismatch for %s: expected table but found %T",
|
||||
rv.Type().String(), mapping)
|
||||
}
|
||||
|
||||
for key, datum := range tmap {
|
||||
@@ -250,14 +263,13 @@ func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error {
|
||||
md.decoded[md.context.add(key).String()] = true
|
||||
md.context = append(md.context, key)
|
||||
if err := md.unify(datum, subv); err != nil {
|
||||
return e("Type mismatch for '%s.%s': %s",
|
||||
rv.Type().String(), f.name, err)
|
||||
return err
|
||||
}
|
||||
md.context = md.context[0 : len(md.context)-1]
|
||||
} else if f.name != "" {
|
||||
// Bad user! No soup for you!
|
||||
return e("Field '%s.%s' is unexported, and therefore cannot "+
|
||||
"be loaded with reflection.", rv.Type().String(), f.name)
|
||||
return e("cannot write unexported field %s.%s",
|
||||
rv.Type().String(), f.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -267,6 +279,9 @@ func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error {
|
||||
func (md *MetaData) unifyMap(mapping interface{}, rv reflect.Value) error {
|
||||
tmap, ok := mapping.(map[string]interface{})
|
||||
if !ok {
|
||||
if tmap == nil {
|
||||
return nil
|
||||
}
|
||||
return badtype("map", mapping)
|
||||
}
|
||||
if rv.IsNil() {
|
||||
@@ -292,6 +307,9 @@ func (md *MetaData) unifyMap(mapping interface{}, rv reflect.Value) error {
|
||||
func (md *MetaData) unifyArray(data interface{}, rv reflect.Value) error {
|
||||
datav := reflect.ValueOf(data)
|
||||
if datav.Kind() != reflect.Slice {
|
||||
if !datav.IsValid() {
|
||||
return nil
|
||||
}
|
||||
return badtype("slice", data)
|
||||
}
|
||||
sliceLen := datav.Len()
|
||||
@@ -305,12 +323,16 @@ func (md *MetaData) unifyArray(data interface{}, rv reflect.Value) error {
|
||||
func (md *MetaData) unifySlice(data interface{}, rv reflect.Value) error {
|
||||
datav := reflect.ValueOf(data)
|
||||
if datav.Kind() != reflect.Slice {
|
||||
if !datav.IsValid() {
|
||||
return nil
|
||||
}
|
||||
return badtype("slice", data)
|
||||
}
|
||||
sliceLen := datav.Len()
|
||||
if rv.IsNil() {
|
||||
rv.Set(reflect.MakeSlice(rv.Type(), sliceLen, sliceLen))
|
||||
n := datav.Len()
|
||||
if rv.IsNil() || rv.Cap() < n {
|
||||
rv.Set(reflect.MakeSlice(rv.Type(), n, n))
|
||||
}
|
||||
rv.SetLen(n)
|
||||
return md.unifySliceArray(datav, rv)
|
||||
}
|
||||
|
||||
@@ -365,15 +387,15 @@ func (md *MetaData) unifyInt(data interface{}, rv reflect.Value) error {
|
||||
// No bounds checking necessary.
|
||||
case reflect.Int8:
|
||||
if num < math.MinInt8 || num > math.MaxInt8 {
|
||||
return e("Value '%d' is out of range for int8.", num)
|
||||
return e("value %d is out of range for int8", num)
|
||||
}
|
||||
case reflect.Int16:
|
||||
if num < math.MinInt16 || num > math.MaxInt16 {
|
||||
return e("Value '%d' is out of range for int16.", num)
|
||||
return e("value %d is out of range for int16", num)
|
||||
}
|
||||
case reflect.Int32:
|
||||
if num < math.MinInt32 || num > math.MaxInt32 {
|
||||
return e("Value '%d' is out of range for int32.", num)
|
||||
return e("value %d is out of range for int32", num)
|
||||
}
|
||||
}
|
||||
rv.SetInt(num)
|
||||
@@ -384,15 +406,15 @@ func (md *MetaData) unifyInt(data interface{}, rv reflect.Value) error {
|
||||
// No bounds checking necessary.
|
||||
case reflect.Uint8:
|
||||
if num < 0 || unum > math.MaxUint8 {
|
||||
return e("Value '%d' is out of range for uint8.", num)
|
||||
return e("value %d is out of range for uint8", num)
|
||||
}
|
||||
case reflect.Uint16:
|
||||
if num < 0 || unum > math.MaxUint16 {
|
||||
return e("Value '%d' is out of range for uint16.", num)
|
||||
return e("value %d is out of range for uint16", num)
|
||||
}
|
||||
case reflect.Uint32:
|
||||
if num < 0 || unum > math.MaxUint32 {
|
||||
return e("Value '%d' is out of range for uint32.", num)
|
||||
return e("value %d is out of range for uint32", num)
|
||||
}
|
||||
}
|
||||
rv.SetUint(unum)
|
||||
@@ -458,7 +480,7 @@ func rvalue(v interface{}) reflect.Value {
|
||||
// interest to us (like encoding.TextUnmarshaler).
|
||||
func indirect(v reflect.Value) reflect.Value {
|
||||
if v.Kind() != reflect.Ptr {
|
||||
if v.CanAddr() {
|
||||
if v.CanSet() {
|
||||
pv := v.Addr()
|
||||
if _, ok := pv.Interface().(TextUnmarshaler); ok {
|
||||
return pv
|
||||
@@ -483,10 +505,5 @@ func isUnifiable(rv reflect.Value) bool {
|
||||
}
|
||||
|
||||
func badtype(expected string, data interface{}) error {
|
||||
return e("Expected %s but found '%T'.", expected, data)
|
||||
}
|
||||
|
||||
func mismatch(user reflect.Value, expected string, data interface{}) error {
|
||||
return e("Type mismatch for %s. Expected %s but found '%T'.",
|
||||
user.Type().String(), expected, data)
|
||||
return e("cannot load TOML value of type %T into a Go %s", data, expected)
|
||||
}
|
||||
|
||||
+22
@@ -59,6 +59,28 @@ func (k Key) String() string {
|
||||
return strings.Join(k, ".")
|
||||
}
|
||||
|
||||
func (k Key) maybeQuotedAll() string {
|
||||
var ss []string
|
||||
for i := range k {
|
||||
ss = append(ss, k.maybeQuoted(i))
|
||||
}
|
||||
return strings.Join(ss, ".")
|
||||
}
|
||||
|
||||
func (k Key) maybeQuoted(i int) string {
|
||||
quote := false
|
||||
for _, c := range k[i] {
|
||||
if !isBareKeyChar(c) {
|
||||
quote = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if quote {
|
||||
return "\"" + strings.Replace(k[i], "\"", "\\\"", -1) + "\""
|
||||
}
|
||||
return k[i]
|
||||
}
|
||||
|
||||
func (k Key) add(piece string) Key {
|
||||
newKey := make(Key, len(k)+1)
|
||||
copy(newKey, k)
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ files via reflection. There is also support for delaying decoding with
|
||||
the Primitive type, and querying the set of keys in a TOML document with the
|
||||
MetaData type.
|
||||
|
||||
The specification implemented: https://github.com/mojombo/toml
|
||||
The specification implemented: https://github.com/toml-lang/toml
|
||||
|
||||
The sub-command github.com/BurntSushi/toml/cmd/tomlv can be used to verify
|
||||
whether a file is a valid TOML document. It can also be used to print the
|
||||
|
||||
+118
-65
@@ -16,17 +16,17 @@ type tomlEncodeError struct{ error }
|
||||
|
||||
var (
|
||||
errArrayMixedElementTypes = errors.New(
|
||||
"can't encode array with mixed element types")
|
||||
"toml: cannot encode array with mixed element types")
|
||||
errArrayNilElement = errors.New(
|
||||
"can't encode array with nil element")
|
||||
"toml: cannot encode array with nil element")
|
||||
errNonString = errors.New(
|
||||
"can't encode a map with non-string key type")
|
||||
"toml: cannot encode a map with non-string key type")
|
||||
errAnonNonStruct = errors.New(
|
||||
"can't encode an anonymous field that is not a struct")
|
||||
"toml: cannot encode an anonymous field that is not a struct")
|
||||
errArrayNoTable = errors.New(
|
||||
"TOML array element can't contain a table")
|
||||
"toml: TOML array element cannot contain a table")
|
||||
errNoKey = errors.New(
|
||||
"top-level values must be a Go map or struct")
|
||||
"toml: top-level values must be Go maps or structs")
|
||||
errAnything = errors.New("") // used in testing
|
||||
)
|
||||
|
||||
@@ -118,7 +118,8 @@ func (enc *Encoder) encode(key Key, rv reflect.Value) {
|
||||
|
||||
k := rv.Kind()
|
||||
switch k {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
|
||||
reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
|
||||
reflect.Uint64,
|
||||
reflect.Float32, reflect.Float64, reflect.String, reflect.Bool:
|
||||
@@ -147,7 +148,7 @@ func (enc *Encoder) encode(key Key, rv reflect.Value) {
|
||||
case reflect.Struct:
|
||||
enc.eTable(key, rv)
|
||||
default:
|
||||
panic(e("Unsupported type for key '%s': %s", key, k))
|
||||
panic(e("unsupported type for key '%s': %s", key, k))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +160,7 @@ func (enc *Encoder) eElement(rv reflect.Value) {
|
||||
// Special case time.Time as a primitive. Has to come before
|
||||
// TextMarshaler below because time.Time implements
|
||||
// encoding.TextMarshaler, but we need to always use UTC.
|
||||
enc.wf(v.In(time.FixedZone("UTC", 0)).Format("2006-01-02T15:04:05Z"))
|
||||
enc.wf(v.UTC().Format("2006-01-02T15:04:05Z"))
|
||||
return
|
||||
case TextMarshaler:
|
||||
// Special case. Use text marshaler if it's available for this value.
|
||||
@@ -173,7 +174,8 @@ func (enc *Encoder) eElement(rv reflect.Value) {
|
||||
switch rv.Kind() {
|
||||
case reflect.Bool:
|
||||
enc.wf(strconv.FormatBool(rv.Bool()))
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
|
||||
reflect.Int64:
|
||||
enc.wf(strconv.FormatInt(rv.Int(), 10))
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16,
|
||||
reflect.Uint32, reflect.Uint64:
|
||||
@@ -189,7 +191,7 @@ func (enc *Encoder) eElement(rv reflect.Value) {
|
||||
case reflect.String:
|
||||
enc.writeQuoted(rv.String())
|
||||
default:
|
||||
panic(e("Unexpected primitive type: %s", rv.Kind()))
|
||||
panic(e("unexpected primitive type: %s", rv.Kind()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,28 +225,28 @@ func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) {
|
||||
if len(key) == 0 {
|
||||
encPanic(errNoKey)
|
||||
}
|
||||
panicIfInvalidKey(key, true)
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
trv := rv.Index(i)
|
||||
if isNil(trv) {
|
||||
continue
|
||||
}
|
||||
panicIfInvalidKey(key)
|
||||
enc.newline()
|
||||
enc.wf("%s[[%s]]", enc.indentStr(key), key.String())
|
||||
enc.wf("%s[[%s]]", enc.indentStr(key), key.maybeQuotedAll())
|
||||
enc.newline()
|
||||
enc.eMapOrStruct(key, trv)
|
||||
}
|
||||
}
|
||||
|
||||
func (enc *Encoder) eTable(key Key, rv reflect.Value) {
|
||||
panicIfInvalidKey(key)
|
||||
if len(key) == 1 {
|
||||
// Output an extra new line between top-level tables.
|
||||
// Output an extra newline between top-level tables.
|
||||
// (The newline isn't written if nothing else has been written though.)
|
||||
enc.newline()
|
||||
}
|
||||
if len(key) > 0 {
|
||||
panicIfInvalidKey(key, true)
|
||||
enc.wf("%s[%s]", enc.indentStr(key), key.String())
|
||||
enc.wf("%s[%s]", enc.indentStr(key), key.maybeQuotedAll())
|
||||
enc.newline()
|
||||
}
|
||||
enc.eMapOrStruct(key, rv)
|
||||
@@ -304,19 +306,36 @@ func (enc *Encoder) eStruct(key Key, rv reflect.Value) {
|
||||
addFields = func(rt reflect.Type, rv reflect.Value, start []int) {
|
||||
for i := 0; i < rt.NumField(); i++ {
|
||||
f := rt.Field(i)
|
||||
// skip unexporded fields
|
||||
if f.PkgPath != "" {
|
||||
// skip unexported fields
|
||||
if f.PkgPath != "" && !f.Anonymous {
|
||||
continue
|
||||
}
|
||||
frv := rv.Field(i)
|
||||
if f.Anonymous {
|
||||
frv := eindirect(frv)
|
||||
t := frv.Type()
|
||||
if t.Kind() != reflect.Struct {
|
||||
encPanic(errAnonNonStruct)
|
||||
t := f.Type
|
||||
switch t.Kind() {
|
||||
case reflect.Struct:
|
||||
// Treat anonymous struct fields with
|
||||
// tag names as though they are not
|
||||
// anonymous, like encoding/json does.
|
||||
if getOptions(f.Tag).name == "" {
|
||||
addFields(t, frv, f.Index)
|
||||
continue
|
||||
}
|
||||
case reflect.Ptr:
|
||||
if t.Elem().Kind() == reflect.Struct &&
|
||||
getOptions(f.Tag).name == "" {
|
||||
if !frv.IsNil() {
|
||||
addFields(t.Elem(), frv.Elem(), f.Index)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Fall through to the normal field encoding logic below
|
||||
// for non-struct anonymous fields.
|
||||
}
|
||||
addFields(t, frv, f.Index)
|
||||
} else if typeIsHash(tomlTypeOfGo(frv)) {
|
||||
}
|
||||
|
||||
if typeIsHash(tomlTypeOfGo(frv)) {
|
||||
fieldsSub = append(fieldsSub, append(start, f.Index...))
|
||||
} else {
|
||||
fieldsDirect = append(fieldsDirect, append(start, f.Index...))
|
||||
@@ -334,13 +353,21 @@ func (enc *Encoder) eStruct(key Key, rv reflect.Value) {
|
||||
continue
|
||||
}
|
||||
|
||||
keyName := sft.Tag.Get("toml")
|
||||
if keyName == "-" {
|
||||
opts := getOptions(sft.Tag)
|
||||
if opts.skip {
|
||||
continue
|
||||
}
|
||||
if keyName == "" {
|
||||
keyName = sft.Name
|
||||
keyName := sft.Name
|
||||
if opts.name != "" {
|
||||
keyName = opts.name
|
||||
}
|
||||
if opts.omitempty && isEmpty(sf) {
|
||||
continue
|
||||
}
|
||||
if opts.omitzero && isZero(sf) {
|
||||
continue
|
||||
}
|
||||
|
||||
enc.encode(key.add(keyName), sf)
|
||||
}
|
||||
}
|
||||
@@ -348,10 +375,10 @@ func (enc *Encoder) eStruct(key Key, rv reflect.Value) {
|
||||
writeFields(fieldsSub)
|
||||
}
|
||||
|
||||
// tomlTypeName returns the TOML type name of the Go value's type. It is used to
|
||||
// determine whether the types of array elements are mixed (which is forbidden).
|
||||
// If the Go value is nil, then it is illegal for it to be an array element, and
|
||||
// valueIsNil is returned as true.
|
||||
// tomlTypeName returns the TOML type name of the Go value's type. It is
|
||||
// used to determine whether the types of array elements are mixed (which is
|
||||
// forbidden). If the Go value is nil, then it is illegal for it to be an array
|
||||
// element, and valueIsNil is returned as true.
|
||||
|
||||
// Returns the TOML type of a Go value. The type may be `nil`, which means
|
||||
// no concrete TOML type could be found.
|
||||
@@ -362,7 +389,8 @@ func tomlTypeOfGo(rv reflect.Value) tomlType {
|
||||
switch rv.Kind() {
|
||||
case reflect.Bool:
|
||||
return tomlBool
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
|
||||
reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
|
||||
reflect.Uint64:
|
||||
return tomlInteger
|
||||
@@ -371,9 +399,8 @@ func tomlTypeOfGo(rv reflect.Value) tomlType {
|
||||
case reflect.Array, reflect.Slice:
|
||||
if typeEqual(tomlHash, tomlArrayType(rv)) {
|
||||
return tomlArrayHash
|
||||
} else {
|
||||
return tomlArray
|
||||
}
|
||||
return tomlArray
|
||||
case reflect.Ptr, reflect.Interface:
|
||||
return tomlTypeOfGo(rv.Elem())
|
||||
case reflect.String:
|
||||
@@ -430,6 +457,54 @@ func tomlArrayType(rv reflect.Value) tomlType {
|
||||
return firstType
|
||||
}
|
||||
|
||||
type tagOptions struct {
|
||||
skip bool // "-"
|
||||
name string
|
||||
omitempty bool
|
||||
omitzero bool
|
||||
}
|
||||
|
||||
func getOptions(tag reflect.StructTag) tagOptions {
|
||||
t := tag.Get("toml")
|
||||
if t == "-" {
|
||||
return tagOptions{skip: true}
|
||||
}
|
||||
var opts tagOptions
|
||||
parts := strings.Split(t, ",")
|
||||
opts.name = parts[0]
|
||||
for _, s := range parts[1:] {
|
||||
switch s {
|
||||
case "omitempty":
|
||||
opts.omitempty = true
|
||||
case "omitzero":
|
||||
opts.omitzero = true
|
||||
}
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
func isZero(rv reflect.Value) bool {
|
||||
switch rv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return rv.Int() == 0
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return rv.Uint() == 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return rv.Float() == 0.0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isEmpty(rv reflect.Value) bool {
|
||||
switch rv.Kind() {
|
||||
case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
|
||||
return rv.Len() == 0
|
||||
case reflect.Bool:
|
||||
return !rv.Bool()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (enc *Encoder) newline() {
|
||||
if enc.hasWritten {
|
||||
enc.wf("\n")
|
||||
@@ -440,8 +515,8 @@ func (enc *Encoder) keyEqElement(key Key, val reflect.Value) {
|
||||
if len(key) == 0 {
|
||||
encPanic(errNoKey)
|
||||
}
|
||||
panicIfInvalidKey(key, false)
|
||||
enc.wf("%s%s = ", enc.indentStr(key), key[len(key)-1])
|
||||
panicIfInvalidKey(key)
|
||||
enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1))
|
||||
enc.eElement(val)
|
||||
enc.newline()
|
||||
}
|
||||
@@ -479,37 +554,15 @@ func isNil(rv reflect.Value) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func panicIfInvalidKey(key Key, hash bool) {
|
||||
if hash {
|
||||
for _, k := range key {
|
||||
if !isValidTableName(k) {
|
||||
encPanic(e("Key '%s' is not a valid table name. Table names "+
|
||||
"cannot contain '[', ']' or '.'.", key.String()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !isValidKeyName(key[len(key)-1]) {
|
||||
encPanic(e("Key '%s' is not a name. Key names "+
|
||||
"cannot contain whitespace.", key.String()))
|
||||
func panicIfInvalidKey(key Key) {
|
||||
for _, k := range key {
|
||||
if len(k) == 0 {
|
||||
encPanic(e("Key '%s' is not a valid table name. Key names "+
|
||||
"cannot be empty.", key.maybeQuotedAll()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isValidTableName(s string) bool {
|
||||
if len(s) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, r := range s {
|
||||
if r == '[' || r == ']' || r == '.' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isValidKeyName(s string) bool {
|
||||
if len(s) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return len(s) != 0
|
||||
}
|
||||
|
||||
+2
-2
@@ -14,6 +14,6 @@ import (
|
||||
// so that Go 1.1 can be supported.
|
||||
type TextMarshaler encoding.TextMarshaler
|
||||
|
||||
// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined here
|
||||
// so that Go 1.1 can be supported.
|
||||
// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined
|
||||
// here so that Go 1.1 can be supported.
|
||||
type TextUnmarshaler encoding.TextUnmarshaler
|
||||
|
||||
+2
-2
@@ -11,8 +11,8 @@ type TextMarshaler interface {
|
||||
MarshalText() (text []byte, err error)
|
||||
}
|
||||
|
||||
// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined here
|
||||
// so that Go 1.1 can be supported.
|
||||
// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined
|
||||
// here so that Go 1.1 can be supported.
|
||||
type TextUnmarshaler interface {
|
||||
UnmarshalText(text []byte) error
|
||||
}
|
||||
|
||||
+418
-328
File diff suppressed because it is too large
Load Diff
+215
-67
@@ -2,7 +2,6 @@ package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -67,7 +66,7 @@ func parse(data string) (p *parser, err error) {
|
||||
}
|
||||
|
||||
func (p *parser) panicf(format string, v ...interface{}) {
|
||||
msg := fmt.Sprintf("Near line %d, key '%s': %s",
|
||||
msg := fmt.Sprintf("Near line %d (last key parsed '%s'): %s",
|
||||
p.approxLine, p.current(), fmt.Sprintf(format, v...))
|
||||
panic(parseError(msg))
|
||||
}
|
||||
@@ -75,13 +74,13 @@ func (p *parser) panicf(format string, v ...interface{}) {
|
||||
func (p *parser) next() item {
|
||||
it := p.lx.nextItem()
|
||||
if it.typ == itemError {
|
||||
p.panicf("Near line %d: %s", it.line, it.val)
|
||||
p.panicf("%s", it.val)
|
||||
}
|
||||
return it
|
||||
}
|
||||
|
||||
func (p *parser) bug(format string, v ...interface{}) {
|
||||
log.Fatalf("BUG: %s\n\n", fmt.Sprintf(format, v...))
|
||||
panic(fmt.Sprintf("BUG: "+format+"\n\n", v...))
|
||||
}
|
||||
|
||||
func (p *parser) expect(typ itemType) item {
|
||||
@@ -102,12 +101,12 @@ func (p *parser) topLevel(item item) {
|
||||
p.approxLine = item.line
|
||||
p.expect(itemText)
|
||||
case itemTableStart:
|
||||
kg := p.expect(itemText)
|
||||
kg := p.next()
|
||||
p.approxLine = kg.line
|
||||
|
||||
key := make(Key, 0)
|
||||
for ; kg.typ == itemText; kg = p.next() {
|
||||
key = append(key, kg.val)
|
||||
var key Key
|
||||
for ; kg.typ != itemTableEnd && kg.typ != itemEOF; kg = p.next() {
|
||||
key = append(key, p.keyString(kg))
|
||||
}
|
||||
p.assertEqual(itemTableEnd, kg.typ)
|
||||
|
||||
@@ -115,12 +114,12 @@ func (p *parser) topLevel(item item) {
|
||||
p.setType("", tomlHash)
|
||||
p.ordered = append(p.ordered, key)
|
||||
case itemArrayTableStart:
|
||||
kg := p.expect(itemText)
|
||||
kg := p.next()
|
||||
p.approxLine = kg.line
|
||||
|
||||
key := make(Key, 0)
|
||||
for ; kg.typ == itemText; kg = p.next() {
|
||||
key = append(key, kg.val)
|
||||
var key Key
|
||||
for ; kg.typ != itemArrayTableEnd && kg.typ != itemEOF; kg = p.next() {
|
||||
key = append(key, p.keyString(kg))
|
||||
}
|
||||
p.assertEqual(itemArrayTableEnd, kg.typ)
|
||||
|
||||
@@ -128,29 +127,44 @@ func (p *parser) topLevel(item item) {
|
||||
p.setType("", tomlArrayHash)
|
||||
p.ordered = append(p.ordered, key)
|
||||
case itemKeyStart:
|
||||
kname := p.expect(itemText)
|
||||
p.currentKey = kname.val
|
||||
kname := p.next()
|
||||
p.approxLine = kname.line
|
||||
p.currentKey = p.keyString(kname)
|
||||
|
||||
val, typ := p.value(p.next())
|
||||
p.setValue(p.currentKey, val)
|
||||
p.setType(p.currentKey, typ)
|
||||
p.ordered = append(p.ordered, p.context.add(p.currentKey))
|
||||
|
||||
p.currentKey = ""
|
||||
default:
|
||||
p.bug("Unexpected type at top level: %s", item.typ)
|
||||
}
|
||||
}
|
||||
|
||||
// Gets a string for a key (or part of a key in a table name).
|
||||
func (p *parser) keyString(it item) string {
|
||||
switch it.typ {
|
||||
case itemText:
|
||||
return it.val
|
||||
case itemString, itemMultilineString,
|
||||
itemRawString, itemRawMultilineString:
|
||||
s, _ := p.value(it)
|
||||
return s.(string)
|
||||
default:
|
||||
p.bug("Unexpected key type: %s", it.typ)
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
// value translates an expected value from the lexer into a Go value wrapped
|
||||
// as an empty interface.
|
||||
func (p *parser) value(it item) (interface{}, tomlType) {
|
||||
switch it.typ {
|
||||
case itemString:
|
||||
return p.replaceUnicode(replaceEscapes(it.val)), p.typeOfPrimitive(it)
|
||||
return p.replaceEscapes(it.val), p.typeOfPrimitive(it)
|
||||
case itemMultilineString:
|
||||
return p.replaceUnicode(replaceEscapes(stripFirstNewline(stripEscapedWhitespace(it.val)))), p.typeOfPrimitive(it)
|
||||
trimmed := stripFirstNewline(stripEscapedWhitespace(it.val))
|
||||
return p.replaceEscapes(trimmed), p.typeOfPrimitive(it)
|
||||
case itemRawString:
|
||||
return it.val, p.typeOfPrimitive(it)
|
||||
case itemRawMultilineString:
|
||||
@@ -164,10 +178,18 @@ func (p *parser) value(it item) (interface{}, tomlType) {
|
||||
}
|
||||
p.bug("Expected boolean value, but got '%s'.", it.val)
|
||||
case itemInteger:
|
||||
num, err := strconv.ParseInt(it.val, 10, 64)
|
||||
if !numUnderscoresOK(it.val) {
|
||||
p.panicf("Invalid integer %q: underscores must be surrounded by digits",
|
||||
it.val)
|
||||
}
|
||||
val := strings.Replace(it.val, "_", "", -1)
|
||||
num, err := strconv.ParseInt(val, 10, 64)
|
||||
if err != nil {
|
||||
// See comment below for floats describing why we make a
|
||||
// distinction between a bug and a user error.
|
||||
// Distinguish integer values. Normally, it'd be a bug if the lexer
|
||||
// provides an invalid integer, but it's possible that the number is
|
||||
// out of range of valid values (which the lexer cannot determine).
|
||||
// So mark the former as a bug but the latter as a legitimate user
|
||||
// error.
|
||||
if e, ok := err.(*strconv.NumError); ok &&
|
||||
e.Err == strconv.ErrRange {
|
||||
|
||||
@@ -179,29 +201,57 @@ func (p *parser) value(it item) (interface{}, tomlType) {
|
||||
}
|
||||
return num, p.typeOfPrimitive(it)
|
||||
case itemFloat:
|
||||
num, err := strconv.ParseFloat(it.val, 64)
|
||||
parts := strings.FieldsFunc(it.val, func(r rune) bool {
|
||||
switch r {
|
||||
case '.', 'e', 'E':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
for _, part := range parts {
|
||||
if !numUnderscoresOK(part) {
|
||||
p.panicf("Invalid float %q: underscores must be "+
|
||||
"surrounded by digits", it.val)
|
||||
}
|
||||
}
|
||||
if !numPeriodsOK(it.val) {
|
||||
// As a special case, numbers like '123.' or '1.e2',
|
||||
// which are valid as far as Go/strconv are concerned,
|
||||
// must be rejected because TOML says that a fractional
|
||||
// part consists of '.' followed by 1+ digits.
|
||||
p.panicf("Invalid float %q: '.' must be followed "+
|
||||
"by one or more digits", it.val)
|
||||
}
|
||||
val := strings.Replace(it.val, "_", "", -1)
|
||||
num, err := strconv.ParseFloat(val, 64)
|
||||
if err != nil {
|
||||
// Distinguish float values. Normally, it'd be a bug if the lexer
|
||||
// provides an invalid float, but it's possible that the float is
|
||||
// out of range of valid values (which the lexer cannot determine).
|
||||
// So mark the former as a bug but the latter as a legitimate user
|
||||
// error.
|
||||
//
|
||||
// This is also true for integers.
|
||||
if e, ok := err.(*strconv.NumError); ok &&
|
||||
e.Err == strconv.ErrRange {
|
||||
|
||||
p.panicf("Float '%s' is out of the range of 64-bit "+
|
||||
"IEEE-754 floating-point numbers.", it.val)
|
||||
} else {
|
||||
p.bug("Expected float value, but got '%s'.", it.val)
|
||||
p.panicf("Invalid float value: %q", it.val)
|
||||
}
|
||||
}
|
||||
return num, p.typeOfPrimitive(it)
|
||||
case itemDatetime:
|
||||
t, err := time.Parse("2006-01-02T15:04:05Z", it.val)
|
||||
if err != nil {
|
||||
p.bug("Expected Zulu formatted DateTime, but got '%s'.", it.val)
|
||||
var t time.Time
|
||||
var ok bool
|
||||
var err error
|
||||
for _, format := range []string{
|
||||
"2006-01-02T15:04:05Z07:00",
|
||||
"2006-01-02T15:04:05",
|
||||
"2006-01-02",
|
||||
} {
|
||||
t, err = time.ParseInLocation(format, it.val, time.Local)
|
||||
if err == nil {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
p.panicf("Invalid TOML Datetime: %q.", it.val)
|
||||
}
|
||||
return t, p.typeOfPrimitive(it)
|
||||
case itemArray:
|
||||
@@ -219,11 +269,75 @@ func (p *parser) value(it item) (interface{}, tomlType) {
|
||||
types = append(types, typ)
|
||||
}
|
||||
return array, p.typeOfArray(types)
|
||||
case itemInlineTableStart:
|
||||
var (
|
||||
hash = make(map[string]interface{})
|
||||
outerContext = p.context
|
||||
outerKey = p.currentKey
|
||||
)
|
||||
|
||||
p.context = append(p.context, p.currentKey)
|
||||
p.currentKey = ""
|
||||
for it := p.next(); it.typ != itemInlineTableEnd; it = p.next() {
|
||||
if it.typ != itemKeyStart {
|
||||
p.bug("Expected key start but instead found %q, around line %d",
|
||||
it.val, p.approxLine)
|
||||
}
|
||||
if it.typ == itemCommentStart {
|
||||
p.expect(itemText)
|
||||
continue
|
||||
}
|
||||
|
||||
// retrieve key
|
||||
k := p.next()
|
||||
p.approxLine = k.line
|
||||
kname := p.keyString(k)
|
||||
|
||||
// retrieve value
|
||||
p.currentKey = kname
|
||||
val, typ := p.value(p.next())
|
||||
// make sure we keep metadata up to date
|
||||
p.setType(kname, typ)
|
||||
p.ordered = append(p.ordered, p.context.add(p.currentKey))
|
||||
hash[kname] = val
|
||||
}
|
||||
p.context = outerContext
|
||||
p.currentKey = outerKey
|
||||
return hash, tomlHash
|
||||
}
|
||||
p.bug("Unexpected value type: %s", it.typ)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
// numUnderscoresOK checks whether each underscore in s is surrounded by
|
||||
// characters that are not underscores.
|
||||
func numUnderscoresOK(s string) bool {
|
||||
accept := false
|
||||
for _, r := range s {
|
||||
if r == '_' {
|
||||
if !accept {
|
||||
return false
|
||||
}
|
||||
accept = false
|
||||
continue
|
||||
}
|
||||
accept = true
|
||||
}
|
||||
return accept
|
||||
}
|
||||
|
||||
// numPeriodsOK checks whether every period in s is followed by a digit.
|
||||
func numPeriodsOK(s string) bool {
|
||||
period := false
|
||||
for _, r := range s {
|
||||
if period && !isDigit(r) {
|
||||
return false
|
||||
}
|
||||
period = r == '.'
|
||||
}
|
||||
return !period
|
||||
}
|
||||
|
||||
// establishContext sets the current context of the parser,
|
||||
// where the context is either a hash or an array of hashes. Which one is
|
||||
// set depends on the value of the `array` parameter.
|
||||
@@ -359,7 +473,8 @@ func (p *parser) addImplicit(key Key) {
|
||||
p.implicits[key.String()] = true
|
||||
}
|
||||
|
||||
// removeImplicit stops tagging the given key as having been implicitly created.
|
||||
// removeImplicit stops tagging the given key as having been implicitly
|
||||
// created.
|
||||
func (p *parser) removeImplicit(key Key) {
|
||||
p.implicits[key.String()] = false
|
||||
}
|
||||
@@ -381,64 +496,97 @@ func (p *parser) current() string {
|
||||
return fmt.Sprintf("%s.%s", p.context, p.currentKey)
|
||||
}
|
||||
|
||||
func replaceEscapes(s string) string {
|
||||
return strings.NewReplacer(
|
||||
"\\b", "\u0008",
|
||||
"\\t", "\u0009",
|
||||
"\\n", "\u000A",
|
||||
"\\f", "\u000C",
|
||||
"\\r", "\u000D",
|
||||
"\\\"", "\u0022",
|
||||
"\\/", "\u002F",
|
||||
"\\\\", "\u005C",
|
||||
).Replace(s)
|
||||
}
|
||||
|
||||
func stripFirstNewline(s string) string {
|
||||
if len(s) == 0 || s[0] != '\n' {
|
||||
return s
|
||||
}
|
||||
|
||||
return s[1:len(s)]
|
||||
return s[1:]
|
||||
}
|
||||
|
||||
func stripEscapedWhitespace(s string) string {
|
||||
esc := strings.Split(s, "\\\n")
|
||||
|
||||
if len(esc) > 1 {
|
||||
for i := 1; i < len(esc); i++ {
|
||||
esc[i] = strings.TrimLeftFunc(esc[i], unicode.IsSpace)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(esc, "")
|
||||
}
|
||||
|
||||
func (p *parser) replaceUnicode(s string) string {
|
||||
indexEsc := func() int {
|
||||
return strings.Index(s, "\\u")
|
||||
func (p *parser) replaceEscapes(str string) string {
|
||||
var replaced []rune
|
||||
s := []byte(str)
|
||||
r := 0
|
||||
for r < len(s) {
|
||||
if s[r] != '\\' {
|
||||
c, size := utf8.DecodeRune(s[r:])
|
||||
r += size
|
||||
replaced = append(replaced, c)
|
||||
continue
|
||||
}
|
||||
r += 1
|
||||
if r >= len(s) {
|
||||
p.bug("Escape sequence at end of string.")
|
||||
return ""
|
||||
}
|
||||
switch s[r] {
|
||||
default:
|
||||
p.bug("Expected valid escape code after \\, but got %q.", s[r])
|
||||
return ""
|
||||
case 'b':
|
||||
replaced = append(replaced, rune(0x0008))
|
||||
r += 1
|
||||
case 't':
|
||||
replaced = append(replaced, rune(0x0009))
|
||||
r += 1
|
||||
case 'n':
|
||||
replaced = append(replaced, rune(0x000A))
|
||||
r += 1
|
||||
case 'f':
|
||||
replaced = append(replaced, rune(0x000C))
|
||||
r += 1
|
||||
case 'r':
|
||||
replaced = append(replaced, rune(0x000D))
|
||||
r += 1
|
||||
case '"':
|
||||
replaced = append(replaced, rune(0x0022))
|
||||
r += 1
|
||||
case '\\':
|
||||
replaced = append(replaced, rune(0x005C))
|
||||
r += 1
|
||||
case 'u':
|
||||
// At this point, we know we have a Unicode escape of the form
|
||||
// `uXXXX` at [r, r+5). (Because the lexer guarantees this
|
||||
// for us.)
|
||||
escaped := p.asciiEscapeToUnicode(s[r+1 : r+5])
|
||||
replaced = append(replaced, escaped)
|
||||
r += 5
|
||||
case 'U':
|
||||
// At this point, we know we have a Unicode escape of the form
|
||||
// `uXXXX` at [r, r+9). (Because the lexer guarantees this
|
||||
// for us.)
|
||||
escaped := p.asciiEscapeToUnicode(s[r+1 : r+9])
|
||||
replaced = append(replaced, escaped)
|
||||
r += 9
|
||||
}
|
||||
}
|
||||
for i := indexEsc(); i != -1; i = indexEsc() {
|
||||
asciiBytes := s[i+2 : i+6]
|
||||
s = strings.Replace(s, s[i:i+6], p.asciiEscapeToUnicode(asciiBytes), -1)
|
||||
}
|
||||
return s
|
||||
return string(replaced)
|
||||
}
|
||||
|
||||
func (p *parser) asciiEscapeToUnicode(s string) string {
|
||||
func (p *parser) asciiEscapeToUnicode(bs []byte) rune {
|
||||
s := string(bs)
|
||||
hex, err := strconv.ParseUint(strings.ToLower(s), 16, 32)
|
||||
if err != nil {
|
||||
p.bug("Could not parse '%s' as a hexadecimal number, but the "+
|
||||
"lexer claims it's OK: %s", s, err)
|
||||
}
|
||||
|
||||
// BUG(burntsushi)
|
||||
// I honestly don't understand how this works. I can't seem
|
||||
// to find a way to make this fail. I figured this would fail on invalid
|
||||
// UTF-8 characters like U+DCFF, but it doesn't.
|
||||
r := string(rune(hex))
|
||||
if !utf8.ValidString(r) {
|
||||
if !utf8.ValidRune(rune(hex)) {
|
||||
p.panicf("Escaped character '\\u%s' is not valid UTF-8.", s)
|
||||
}
|
||||
return string(r)
|
||||
return rune(hex)
|
||||
}
|
||||
|
||||
func isStringType(ty itemType) bool {
|
||||
return ty == itemString || ty == itemMultilineString ||
|
||||
ty == itemRawString || ty == itemRawMultilineString
|
||||
}
|
||||
|
||||
+2
-2
@@ -83,8 +83,8 @@ func (p *parser) typeOfArray(types []tomlType) tomlType {
|
||||
theType := types[0]
|
||||
for _, t := range types[1:] {
|
||||
if !typeEqual(theType, t) {
|
||||
p.panicf("Array contains values of type '%s' and '%s', but arrays "+
|
||||
"must be homogeneous.", theType, t)
|
||||
p.panicf("Array contains values of type '%s' and '%s', but "+
|
||||
"arrays must be homogeneous.", theType, t)
|
||||
}
|
||||
}
|
||||
return tomlArray
|
||||
|
||||
+6
-5
@@ -92,11 +92,11 @@ func typeFields(t reflect.Type) []field {
|
||||
// Scan f.typ for fields to include.
|
||||
for i := 0; i < f.typ.NumField(); i++ {
|
||||
sf := f.typ.Field(i)
|
||||
if sf.PkgPath != "" { // unexported
|
||||
if sf.PkgPath != "" && !sf.Anonymous { // unexported
|
||||
continue
|
||||
}
|
||||
name := sf.Tag.Get("toml")
|
||||
if name == "-" {
|
||||
opts := getOptions(sf.Tag)
|
||||
if opts.skip {
|
||||
continue
|
||||
}
|
||||
index := make([]int, len(f.index)+1)
|
||||
@@ -110,8 +110,9 @@ func typeFields(t reflect.Type) []field {
|
||||
}
|
||||
|
||||
// Record found field and index sequence.
|
||||
if name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct {
|
||||
tagged := name != ""
|
||||
if opts.name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct {
|
||||
tagged := opts.name != ""
|
||||
name := opts.name
|
||||
if name == "" {
|
||||
name = sf.Name
|
||||
}
|
||||
|
||||
+4
-2
@@ -1,6 +1,8 @@
|
||||
Copyright (c) 2012-2013 Dave Collins <dave@davec.name>
|
||||
ISC License
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
Copyright (c) 2012-2016 Dave Collins <dave@davec.name>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
|
||||
+94
-100
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2015 Dave Collins <dave@davec.name>
|
||||
// Copyright (c) 2015-2016 Dave Collins <dave@davec.name>
|
||||
//
|
||||
// Permission to use, copy, modify, and distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
@@ -13,9 +13,12 @@
|
||||
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
// NOTE: Due to the following build constraints, this file will only be compiled
|
||||
// when the code is not running on Google App Engine and "-tags disableunsafe"
|
||||
// is not added to the go build command line.
|
||||
// +build !appengine,!disableunsafe
|
||||
// when the code is not running on Google App Engine, compiled by GopherJS, and
|
||||
// "-tags safe" is not added to the go build command line. The "disableunsafe"
|
||||
// tag is deprecated and thus should not be used.
|
||||
// Go versions prior to 1.4 are disabled because they use a different layout
|
||||
// for interfaces which make the implementation of unsafeReflectValue more complex.
|
||||
// +build !js,!appengine,!safe,!disableunsafe,go1.4
|
||||
|
||||
package spew
|
||||
|
||||
@@ -33,80 +36,49 @@ const (
|
||||
ptrSize = unsafe.Sizeof((*byte)(nil))
|
||||
)
|
||||
|
||||
var (
|
||||
// offsetPtr, offsetScalar, and offsetFlag are the offsets for the
|
||||
// internal reflect.Value fields. These values are valid before golang
|
||||
// commit ecccf07e7f9d which changed the format. The are also valid
|
||||
// after commit 82f48826c6c7 which changed the format again to mirror
|
||||
// the original format. Code in the init function updates these offsets
|
||||
// as necessary.
|
||||
offsetPtr = uintptr(ptrSize)
|
||||
offsetScalar = uintptr(0)
|
||||
offsetFlag = uintptr(ptrSize * 2)
|
||||
type flag uintptr
|
||||
|
||||
// flagKindWidth and flagKindShift indicate various bits that the
|
||||
// reflect package uses internally to track kind information.
|
||||
//
|
||||
// flagRO indicates whether or not the value field of a reflect.Value is
|
||||
// read-only.
|
||||
//
|
||||
// flagIndir indicates whether the value field of a reflect.Value is
|
||||
// the actual data or a pointer to the data.
|
||||
//
|
||||
// These values are valid before golang commit 90a7c3c86944 which
|
||||
// changed their positions. Code in the init function updates these
|
||||
// flags as necessary.
|
||||
flagKindWidth = uintptr(5)
|
||||
flagKindShift = uintptr(flagKindWidth - 1)
|
||||
flagRO = uintptr(1 << 0)
|
||||
flagIndir = uintptr(1 << 1)
|
||||
var (
|
||||
// flagRO indicates whether the value field of a reflect.Value
|
||||
// is read-only.
|
||||
flagRO flag
|
||||
|
||||
// flagAddr indicates whether the address of the reflect.Value's
|
||||
// value may be taken.
|
||||
flagAddr flag
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Older versions of reflect.Value stored small integers directly in the
|
||||
// ptr field (which is named val in the older versions). Versions
|
||||
// between commits ecccf07e7f9d and 82f48826c6c7 added a new field named
|
||||
// scalar for this purpose which unfortunately came before the flag
|
||||
// field, so the offset of the flag field is different for those
|
||||
// versions.
|
||||
//
|
||||
// This code constructs a new reflect.Value from a known small integer
|
||||
// and checks if the size of the reflect.Value struct indicates it has
|
||||
// the scalar field. When it does, the offsets are updated accordingly.
|
||||
vv := reflect.ValueOf(0xf00)
|
||||
if unsafe.Sizeof(vv) == (ptrSize * 4) {
|
||||
offsetScalar = ptrSize * 2
|
||||
offsetFlag = ptrSize * 3
|
||||
}
|
||||
// flagKindMask holds the bits that make up the kind
|
||||
// part of the flags field. In all the supported versions,
|
||||
// it is in the lower 5 bits.
|
||||
const flagKindMask = flag(0x1f)
|
||||
|
||||
// Commit 90a7c3c86944 changed the flag positions such that the low
|
||||
// order bits are the kind. This code extracts the kind from the flags
|
||||
// field and ensures it's the correct type. When it's not, the flag
|
||||
// order has been changed to the newer format, so the flags are updated
|
||||
// accordingly.
|
||||
upf := unsafe.Pointer(uintptr(unsafe.Pointer(&vv)) + offsetFlag)
|
||||
upfv := *(*uintptr)(upf)
|
||||
flagKindMask := uintptr((1<<flagKindWidth - 1) << flagKindShift)
|
||||
if (upfv&flagKindMask)>>flagKindShift != uintptr(reflect.Int) {
|
||||
flagKindShift = 0
|
||||
flagRO = 1 << 5
|
||||
flagIndir = 1 << 6
|
||||
// Different versions of Go have used different
|
||||
// bit layouts for the flags type. This table
|
||||
// records the known combinations.
|
||||
var okFlags = []struct {
|
||||
ro, addr flag
|
||||
}{{
|
||||
// From Go 1.4 to 1.5
|
||||
ro: 1 << 5,
|
||||
addr: 1 << 7,
|
||||
}, {
|
||||
// Up to Go tip.
|
||||
ro: 1<<5 | 1<<6,
|
||||
addr: 1 << 8,
|
||||
}}
|
||||
|
||||
// Commit adf9b30e5594 modified the flags to separate the
|
||||
// flagRO flag into two bits which specifies whether or not the
|
||||
// field is embedded. This causes flagIndir to move over a bit
|
||||
// and means that flagRO is the combination of either of the
|
||||
// original flagRO bit and the new bit.
|
||||
//
|
||||
// This code detects the change by extracting what used to be
|
||||
// the indirect bit to ensure it's set. When it's not, the flag
|
||||
// order has been changed to the newer format, so the flags are
|
||||
// updated accordingly.
|
||||
if upfv&flagIndir == 0 {
|
||||
flagRO = 3 << 5
|
||||
flagIndir = 1 << 7
|
||||
}
|
||||
var flagValOffset = func() uintptr {
|
||||
field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
|
||||
if !ok {
|
||||
panic("reflect.Value has no flag field")
|
||||
}
|
||||
return field.Offset
|
||||
}()
|
||||
|
||||
// flagField returns a pointer to the flag field of a reflect.Value.
|
||||
func flagField(v *reflect.Value) *flag {
|
||||
return (*flag)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + flagValOffset))
|
||||
}
|
||||
|
||||
// unsafeReflectValue converts the passed reflect.Value into a one that bypasses
|
||||
@@ -118,34 +90,56 @@ func init() {
|
||||
// This allows us to check for implementations of the Stringer and error
|
||||
// interfaces to be used for pretty printing ordinarily unaddressable and
|
||||
// inaccessible values such as unexported struct fields.
|
||||
func unsafeReflectValue(v reflect.Value) (rv reflect.Value) {
|
||||
indirects := 1
|
||||
vt := v.Type()
|
||||
upv := unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetPtr)
|
||||
rvf := *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetFlag))
|
||||
if rvf&flagIndir != 0 {
|
||||
vt = reflect.PtrTo(v.Type())
|
||||
indirects++
|
||||
} else if offsetScalar != 0 {
|
||||
// The value is in the scalar field when it's not one of the
|
||||
// reference types.
|
||||
switch vt.Kind() {
|
||||
case reflect.Uintptr:
|
||||
case reflect.Chan:
|
||||
case reflect.Func:
|
||||
case reflect.Map:
|
||||
case reflect.Ptr:
|
||||
case reflect.UnsafePointer:
|
||||
default:
|
||||
upv = unsafe.Pointer(uintptr(unsafe.Pointer(&v)) +
|
||||
offsetScalar)
|
||||
func unsafeReflectValue(v reflect.Value) reflect.Value {
|
||||
if !v.IsValid() || (v.CanInterface() && v.CanAddr()) {
|
||||
return v
|
||||
}
|
||||
flagFieldPtr := flagField(&v)
|
||||
*flagFieldPtr &^= flagRO
|
||||
*flagFieldPtr |= flagAddr
|
||||
return v
|
||||
}
|
||||
|
||||
// Sanity checks against future reflect package changes
|
||||
// to the type or semantics of the Value.flag field.
|
||||
func init() {
|
||||
field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
|
||||
if !ok {
|
||||
panic("reflect.Value has no flag field")
|
||||
}
|
||||
if field.Type.Kind() != reflect.TypeOf(flag(0)).Kind() {
|
||||
panic("reflect.Value flag field has changed kind")
|
||||
}
|
||||
type t0 int
|
||||
var t struct {
|
||||
A t0
|
||||
// t0 will have flagEmbedRO set.
|
||||
t0
|
||||
// a will have flagStickyRO set
|
||||
a t0
|
||||
}
|
||||
vA := reflect.ValueOf(t).FieldByName("A")
|
||||
va := reflect.ValueOf(t).FieldByName("a")
|
||||
vt0 := reflect.ValueOf(t).FieldByName("t0")
|
||||
|
||||
// Infer flagRO from the difference between the flags
|
||||
// for the (otherwise identical) fields in t.
|
||||
flagPublic := *flagField(&vA)
|
||||
flagWithRO := *flagField(&va) | *flagField(&vt0)
|
||||
flagRO = flagPublic ^ flagWithRO
|
||||
|
||||
// Infer flagAddr from the difference between a value
|
||||
// taken from a pointer and not.
|
||||
vPtrA := reflect.ValueOf(&t).Elem().FieldByName("A")
|
||||
flagNoPtr := *flagField(&vA)
|
||||
flagPtr := *flagField(&vPtrA)
|
||||
flagAddr = flagNoPtr ^ flagPtr
|
||||
|
||||
// Check that the inferred flags tally with one of the known versions.
|
||||
for _, f := range okFlags {
|
||||
if flagRO == f.ro && flagAddr == f.addr {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
pv := reflect.NewAt(vt, upv)
|
||||
rv = pv
|
||||
for i := 0; i < indirects; i++ {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
return rv
|
||||
panic("reflect.Value read-only flag has changed semantics")
|
||||
}
|
||||
|
||||
+5
-4
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2015 Dave Collins <dave@davec.name>
|
||||
// Copyright (c) 2015-2016 Dave Collins <dave@davec.name>
|
||||
//
|
||||
// Permission to use, copy, modify, and distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
@@ -13,9 +13,10 @@
|
||||
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
// NOTE: Due to the following build constraints, this file will only be compiled
|
||||
// when either the code is running on Google App Engine or "-tags disableunsafe"
|
||||
// is added to the go build command line.
|
||||
// +build appengine disableunsafe
|
||||
// when the code is running on Google App Engine, compiled by GopherJS, or
|
||||
// "-tags safe" is added to the go build command line. The "disableunsafe"
|
||||
// tag is deprecated and thus should not be used.
|
||||
// +build js appengine safe disableunsafe !go1.4
|
||||
|
||||
package spew
|
||||
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2013 Dave Collins <dave@davec.name>
|
||||
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
@@ -180,7 +180,7 @@ func printComplex(w io.Writer, c complex128, floatPrecision int) {
|
||||
w.Write(closeParenBytes)
|
||||
}
|
||||
|
||||
// printHexPtr outputs a uintptr formatted as hexidecimal with a leading '0x'
|
||||
// printHexPtr outputs a uintptr formatted as hexadecimal with a leading '0x'
|
||||
// prefix to Writer w.
|
||||
func printHexPtr(w io.Writer, p uintptr) {
|
||||
// Null pointer.
|
||||
|
||||
-298
@@ -1,298 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2013 Dave Collins <dave@davec.name>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
package spew_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
)
|
||||
|
||||
// custom type to test Stinger interface on non-pointer receiver.
|
||||
type stringer string
|
||||
|
||||
// String implements the Stringer interface for testing invocation of custom
|
||||
// stringers on types with non-pointer receivers.
|
||||
func (s stringer) String() string {
|
||||
return "stringer " + string(s)
|
||||
}
|
||||
|
||||
// custom type to test Stinger interface on pointer receiver.
|
||||
type pstringer string
|
||||
|
||||
// String implements the Stringer interface for testing invocation of custom
|
||||
// stringers on types with only pointer receivers.
|
||||
func (s *pstringer) String() string {
|
||||
return "stringer " + string(*s)
|
||||
}
|
||||
|
||||
// xref1 and xref2 are cross referencing structs for testing circular reference
|
||||
// detection.
|
||||
type xref1 struct {
|
||||
ps2 *xref2
|
||||
}
|
||||
type xref2 struct {
|
||||
ps1 *xref1
|
||||
}
|
||||
|
||||
// indirCir1, indirCir2, and indirCir3 are used to generate an indirect circular
|
||||
// reference for testing detection.
|
||||
type indirCir1 struct {
|
||||
ps2 *indirCir2
|
||||
}
|
||||
type indirCir2 struct {
|
||||
ps3 *indirCir3
|
||||
}
|
||||
type indirCir3 struct {
|
||||
ps1 *indirCir1
|
||||
}
|
||||
|
||||
// embed is used to test embedded structures.
|
||||
type embed struct {
|
||||
a string
|
||||
}
|
||||
|
||||
// embedwrap is used to test embedded structures.
|
||||
type embedwrap struct {
|
||||
*embed
|
||||
e *embed
|
||||
}
|
||||
|
||||
// panicer is used to intentionally cause a panic for testing spew properly
|
||||
// handles them
|
||||
type panicer int
|
||||
|
||||
func (p panicer) String() string {
|
||||
panic("test panic")
|
||||
}
|
||||
|
||||
// customError is used to test custom error interface invocation.
|
||||
type customError int
|
||||
|
||||
func (e customError) Error() string {
|
||||
return fmt.Sprintf("error: %d", int(e))
|
||||
}
|
||||
|
||||
// stringizeWants converts a slice of wanted test output into a format suitable
|
||||
// for a test error message.
|
||||
func stringizeWants(wants []string) string {
|
||||
s := ""
|
||||
for i, want := range wants {
|
||||
if i > 0 {
|
||||
s += fmt.Sprintf("want%d: %s", i+1, want)
|
||||
} else {
|
||||
s += "want: " + want
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// testFailed returns whether or not a test failed by checking if the result
|
||||
// of the test is in the slice of wanted strings.
|
||||
func testFailed(result string, wants []string) bool {
|
||||
for _, want := range wants {
|
||||
if result == want {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type sortableStruct struct {
|
||||
x int
|
||||
}
|
||||
|
||||
func (ss sortableStruct) String() string {
|
||||
return fmt.Sprintf("ss.%d", ss.x)
|
||||
}
|
||||
|
||||
type unsortableStruct struct {
|
||||
x int
|
||||
}
|
||||
|
||||
type sortTestCase struct {
|
||||
input []reflect.Value
|
||||
expected []reflect.Value
|
||||
}
|
||||
|
||||
func helpTestSortValues(tests []sortTestCase, cs *spew.ConfigState, t *testing.T) {
|
||||
getInterfaces := func(values []reflect.Value) []interface{} {
|
||||
interfaces := []interface{}{}
|
||||
for _, v := range values {
|
||||
interfaces = append(interfaces, v.Interface())
|
||||
}
|
||||
return interfaces
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
spew.SortValues(test.input, cs)
|
||||
// reflect.DeepEqual cannot really make sense of reflect.Value,
|
||||
// probably because of all the pointer tricks. For instance,
|
||||
// v(2.0) != v(2.0) on a 32-bits system. Turn them into interface{}
|
||||
// instead.
|
||||
input := getInterfaces(test.input)
|
||||
expected := getInterfaces(test.expected)
|
||||
if !reflect.DeepEqual(input, expected) {
|
||||
t.Errorf("Sort mismatch:\n %v != %v", input, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSortValues ensures the sort functionality for relect.Value based sorting
|
||||
// works as intended.
|
||||
func TestSortValues(t *testing.T) {
|
||||
v := reflect.ValueOf
|
||||
|
||||
a := v("a")
|
||||
b := v("b")
|
||||
c := v("c")
|
||||
embedA := v(embed{"a"})
|
||||
embedB := v(embed{"b"})
|
||||
embedC := v(embed{"c"})
|
||||
tests := []sortTestCase{
|
||||
// No values.
|
||||
{
|
||||
[]reflect.Value{},
|
||||
[]reflect.Value{},
|
||||
},
|
||||
// Bools.
|
||||
{
|
||||
[]reflect.Value{v(false), v(true), v(false)},
|
||||
[]reflect.Value{v(false), v(false), v(true)},
|
||||
},
|
||||
// Ints.
|
||||
{
|
||||
[]reflect.Value{v(2), v(1), v(3)},
|
||||
[]reflect.Value{v(1), v(2), v(3)},
|
||||
},
|
||||
// Uints.
|
||||
{
|
||||
[]reflect.Value{v(uint8(2)), v(uint8(1)), v(uint8(3))},
|
||||
[]reflect.Value{v(uint8(1)), v(uint8(2)), v(uint8(3))},
|
||||
},
|
||||
// Floats.
|
||||
{
|
||||
[]reflect.Value{v(2.0), v(1.0), v(3.0)},
|
||||
[]reflect.Value{v(1.0), v(2.0), v(3.0)},
|
||||
},
|
||||
// Strings.
|
||||
{
|
||||
[]reflect.Value{b, a, c},
|
||||
[]reflect.Value{a, b, c},
|
||||
},
|
||||
// Array
|
||||
{
|
||||
[]reflect.Value{v([3]int{3, 2, 1}), v([3]int{1, 3, 2}), v([3]int{1, 2, 3})},
|
||||
[]reflect.Value{v([3]int{1, 2, 3}), v([3]int{1, 3, 2}), v([3]int{3, 2, 1})},
|
||||
},
|
||||
// Uintptrs.
|
||||
{
|
||||
[]reflect.Value{v(uintptr(2)), v(uintptr(1)), v(uintptr(3))},
|
||||
[]reflect.Value{v(uintptr(1)), v(uintptr(2)), v(uintptr(3))},
|
||||
},
|
||||
// SortableStructs.
|
||||
{
|
||||
// Note: not sorted - DisableMethods is set.
|
||||
[]reflect.Value{v(sortableStruct{2}), v(sortableStruct{1}), v(sortableStruct{3})},
|
||||
[]reflect.Value{v(sortableStruct{2}), v(sortableStruct{1}), v(sortableStruct{3})},
|
||||
},
|
||||
// UnsortableStructs.
|
||||
{
|
||||
// Note: not sorted - SpewKeys is false.
|
||||
[]reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})},
|
||||
[]reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})},
|
||||
},
|
||||
// Invalid.
|
||||
{
|
||||
[]reflect.Value{embedB, embedA, embedC},
|
||||
[]reflect.Value{embedB, embedA, embedC},
|
||||
},
|
||||
}
|
||||
cs := spew.ConfigState{DisableMethods: true, SpewKeys: false}
|
||||
helpTestSortValues(tests, &cs, t)
|
||||
}
|
||||
|
||||
// TestSortValuesWithMethods ensures the sort functionality for relect.Value
|
||||
// based sorting works as intended when using string methods.
|
||||
func TestSortValuesWithMethods(t *testing.T) {
|
||||
v := reflect.ValueOf
|
||||
|
||||
a := v("a")
|
||||
b := v("b")
|
||||
c := v("c")
|
||||
tests := []sortTestCase{
|
||||
// Ints.
|
||||
{
|
||||
[]reflect.Value{v(2), v(1), v(3)},
|
||||
[]reflect.Value{v(1), v(2), v(3)},
|
||||
},
|
||||
// Strings.
|
||||
{
|
||||
[]reflect.Value{b, a, c},
|
||||
[]reflect.Value{a, b, c},
|
||||
},
|
||||
// SortableStructs.
|
||||
{
|
||||
[]reflect.Value{v(sortableStruct{2}), v(sortableStruct{1}), v(sortableStruct{3})},
|
||||
[]reflect.Value{v(sortableStruct{1}), v(sortableStruct{2}), v(sortableStruct{3})},
|
||||
},
|
||||
// UnsortableStructs.
|
||||
{
|
||||
// Note: not sorted - SpewKeys is false.
|
||||
[]reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})},
|
||||
[]reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})},
|
||||
},
|
||||
}
|
||||
cs := spew.ConfigState{DisableMethods: false, SpewKeys: false}
|
||||
helpTestSortValues(tests, &cs, t)
|
||||
}
|
||||
|
||||
// TestSortValuesWithSpew ensures the sort functionality for relect.Value
|
||||
// based sorting works as intended when using spew to stringify keys.
|
||||
func TestSortValuesWithSpew(t *testing.T) {
|
||||
v := reflect.ValueOf
|
||||
|
||||
a := v("a")
|
||||
b := v("b")
|
||||
c := v("c")
|
||||
tests := []sortTestCase{
|
||||
// Ints.
|
||||
{
|
||||
[]reflect.Value{v(2), v(1), v(3)},
|
||||
[]reflect.Value{v(1), v(2), v(3)},
|
||||
},
|
||||
// Strings.
|
||||
{
|
||||
[]reflect.Value{b, a, c},
|
||||
[]reflect.Value{a, b, c},
|
||||
},
|
||||
// SortableStructs.
|
||||
{
|
||||
[]reflect.Value{v(sortableStruct{2}), v(sortableStruct{1}), v(sortableStruct{3})},
|
||||
[]reflect.Value{v(sortableStruct{1}), v(sortableStruct{2}), v(sortableStruct{3})},
|
||||
},
|
||||
// UnsortableStructs.
|
||||
{
|
||||
[]reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})},
|
||||
[]reflect.Value{v(unsortableStruct{1}), v(unsortableStruct{2}), v(unsortableStruct{3})},
|
||||
},
|
||||
}
|
||||
cs := spew.ConfigState{DisableMethods: true, SpewKeys: true}
|
||||
helpTestSortValues(tests, &cs, t)
|
||||
}
|
||||
+11
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2013 Dave Collins <dave@davec.name>
|
||||
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
@@ -64,9 +64,18 @@ type ConfigState struct {
|
||||
// inside these interface methods. As a result, this option relies on
|
||||
// access to the unsafe package, so it will not have any effect when
|
||||
// running in environments without access to the unsafe package such as
|
||||
// Google App Engine or with the "disableunsafe" build tag specified.
|
||||
// Google App Engine or with the "safe" build tag specified.
|
||||
DisablePointerMethods bool
|
||||
|
||||
// DisablePointerAddresses specifies whether to disable the printing of
|
||||
// pointer addresses. This is useful when diffing data structures in tests.
|
||||
DisablePointerAddresses bool
|
||||
|
||||
// DisableCapacities specifies whether to disable the printing of capacities
|
||||
// for arrays, slices, maps and channels. This is useful when diffing
|
||||
// data structures in tests.
|
||||
DisableCapacities bool
|
||||
|
||||
// ContinueOnMethod specifies whether or not recursion should continue once
|
||||
// a custom error or Stringer interface is invoked. The default, false,
|
||||
// means it will print the results of invoking the custom error or Stringer
|
||||
|
||||
+10
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2013 Dave Collins <dave@davec.name>
|
||||
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
@@ -91,6 +91,15 @@ The following configuration options are available:
|
||||
which only accept pointer receivers from non-pointer variables.
|
||||
Pointer method invocation is enabled by default.
|
||||
|
||||
* DisablePointerAddresses
|
||||
DisablePointerAddresses specifies whether to disable the printing of
|
||||
pointer addresses. This is useful when diffing data structures in tests.
|
||||
|
||||
* DisableCapacities
|
||||
DisableCapacities specifies whether to disable the printing of
|
||||
capacities for arrays, slices, maps and channels. This is useful when
|
||||
diffing data structures in tests.
|
||||
|
||||
* ContinueOnMethod
|
||||
Enables recursion into types after invoking error and Stringer interface
|
||||
methods. Recursion after method invocation is disabled by default.
|
||||
|
||||
+9
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2013 Dave Collins <dave@davec.name>
|
||||
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
@@ -35,16 +35,16 @@ var (
|
||||
|
||||
// cCharRE is a regular expression that matches a cgo char.
|
||||
// It is used to detect character arrays to hexdump them.
|
||||
cCharRE = regexp.MustCompile("^.*\\._Ctype_char$")
|
||||
cCharRE = regexp.MustCompile(`^.*\._Ctype_char$`)
|
||||
|
||||
// cUnsignedCharRE is a regular expression that matches a cgo unsigned
|
||||
// char. It is used to detect unsigned character arrays to hexdump
|
||||
// them.
|
||||
cUnsignedCharRE = regexp.MustCompile("^.*\\._Ctype_unsignedchar$")
|
||||
cUnsignedCharRE = regexp.MustCompile(`^.*\._Ctype_unsignedchar$`)
|
||||
|
||||
// cUint8tCharRE is a regular expression that matches a cgo uint8_t.
|
||||
// It is used to detect uint8_t arrays to hexdump them.
|
||||
cUint8tCharRE = regexp.MustCompile("^.*\\._Ctype_uint8_t$")
|
||||
cUint8tCharRE = regexp.MustCompile(`^.*\._Ctype_uint8_t$`)
|
||||
)
|
||||
|
||||
// dumpState contains information about the state of a dump operation.
|
||||
@@ -129,7 +129,7 @@ func (d *dumpState) dumpPtr(v reflect.Value) {
|
||||
d.w.Write(closeParenBytes)
|
||||
|
||||
// Display pointer information.
|
||||
if len(pointerChain) > 0 {
|
||||
if !d.cs.DisablePointerAddresses && len(pointerChain) > 0 {
|
||||
d.w.Write(openParenBytes)
|
||||
for i, addr := range pointerChain {
|
||||
if i > 0 {
|
||||
@@ -143,10 +143,10 @@ func (d *dumpState) dumpPtr(v reflect.Value) {
|
||||
// Display dereferenced value.
|
||||
d.w.Write(openParenBytes)
|
||||
switch {
|
||||
case nilFound == true:
|
||||
case nilFound:
|
||||
d.w.Write(nilAngleBytes)
|
||||
|
||||
case cycleFound == true:
|
||||
case cycleFound:
|
||||
d.w.Write(circularBytes)
|
||||
|
||||
default:
|
||||
@@ -282,13 +282,13 @@ func (d *dumpState) dump(v reflect.Value) {
|
||||
case reflect.Map, reflect.String:
|
||||
valueLen = v.Len()
|
||||
}
|
||||
if valueLen != 0 || valueCap != 0 {
|
||||
if valueLen != 0 || !d.cs.DisableCapacities && valueCap != 0 {
|
||||
d.w.Write(openParenBytes)
|
||||
if valueLen != 0 {
|
||||
d.w.Write(lenEqualsBytes)
|
||||
printInt(d.w, int64(valueLen), 10)
|
||||
}
|
||||
if valueCap != 0 {
|
||||
if !d.cs.DisableCapacities && valueCap != 0 {
|
||||
if valueLen != 0 {
|
||||
d.w.Write(spaceBytes)
|
||||
}
|
||||
|
||||
-1042
File diff suppressed because it is too large
Load Diff
-98
@@ -1,98 +0,0 @@
|
||||
// Copyright (c) 2013 Dave Collins <dave@davec.name>
|
||||
//
|
||||
// Permission to use, copy, modify, and distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
// NOTE: Due to the following build constraints, this file will only be compiled
|
||||
// when both cgo is supported and "-tags testcgo" is added to the go test
|
||||
// command line. This means the cgo tests are only added (and hence run) when
|
||||
// specifially requested. This configuration is used because spew itself
|
||||
// does not require cgo to run even though it does handle certain cgo types
|
||||
// specially. Rather than forcing all clients to require cgo and an external
|
||||
// C compiler just to run the tests, this scheme makes them optional.
|
||||
// +build cgo,testcgo
|
||||
|
||||
package spew_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/davecgh/go-spew/spew/testdata"
|
||||
)
|
||||
|
||||
func addCgoDumpTests() {
|
||||
// C char pointer.
|
||||
v := testdata.GetCgoCharPointer()
|
||||
nv := testdata.GetCgoNullCharPointer()
|
||||
pv := &v
|
||||
vcAddr := fmt.Sprintf("%p", v)
|
||||
vAddr := fmt.Sprintf("%p", pv)
|
||||
pvAddr := fmt.Sprintf("%p", &pv)
|
||||
vt := "*testdata._Ctype_char"
|
||||
vs := "116"
|
||||
addDumpTest(v, "("+vt+")("+vcAddr+")("+vs+")\n")
|
||||
addDumpTest(pv, "(*"+vt+")("+vAddr+"->"+vcAddr+")("+vs+")\n")
|
||||
addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+"->"+vcAddr+")("+vs+")\n")
|
||||
addDumpTest(nv, "("+vt+")(<nil>)\n")
|
||||
|
||||
// C char array.
|
||||
v2, v2l, v2c := testdata.GetCgoCharArray()
|
||||
v2Len := fmt.Sprintf("%d", v2l)
|
||||
v2Cap := fmt.Sprintf("%d", v2c)
|
||||
v2t := "[6]testdata._Ctype_char"
|
||||
v2s := "(len=" + v2Len + " cap=" + v2Cap + ") " +
|
||||
"{\n 00000000 74 65 73 74 32 00 " +
|
||||
" |test2.|\n}"
|
||||
addDumpTest(v2, "("+v2t+") "+v2s+"\n")
|
||||
|
||||
// C unsigned char array.
|
||||
v3, v3l, v3c := testdata.GetCgoUnsignedCharArray()
|
||||
v3Len := fmt.Sprintf("%d", v3l)
|
||||
v3Cap := fmt.Sprintf("%d", v3c)
|
||||
v3t := "[6]testdata._Ctype_unsignedchar"
|
||||
v3s := "(len=" + v3Len + " cap=" + v3Cap + ") " +
|
||||
"{\n 00000000 74 65 73 74 33 00 " +
|
||||
" |test3.|\n}"
|
||||
addDumpTest(v3, "("+v3t+") "+v3s+"\n")
|
||||
|
||||
// C signed char array.
|
||||
v4, v4l, v4c := testdata.GetCgoSignedCharArray()
|
||||
v4Len := fmt.Sprintf("%d", v4l)
|
||||
v4Cap := fmt.Sprintf("%d", v4c)
|
||||
v4t := "[6]testdata._Ctype_schar"
|
||||
v4t2 := "testdata._Ctype_schar"
|
||||
v4s := "(len=" + v4Len + " cap=" + v4Cap + ") " +
|
||||
"{\n (" + v4t2 + ") 116,\n (" + v4t2 + ") 101,\n (" + v4t2 +
|
||||
") 115,\n (" + v4t2 + ") 116,\n (" + v4t2 + ") 52,\n (" + v4t2 +
|
||||
") 0\n}"
|
||||
addDumpTest(v4, "("+v4t+") "+v4s+"\n")
|
||||
|
||||
// C uint8_t array.
|
||||
v5, v5l, v5c := testdata.GetCgoUint8tArray()
|
||||
v5Len := fmt.Sprintf("%d", v5l)
|
||||
v5Cap := fmt.Sprintf("%d", v5c)
|
||||
v5t := "[6]testdata._Ctype_uint8_t"
|
||||
v5s := "(len=" + v5Len + " cap=" + v5Cap + ") " +
|
||||
"{\n 00000000 74 65 73 74 35 00 " +
|
||||
" |test5.|\n}"
|
||||
addDumpTest(v5, "("+v5t+") "+v5s+"\n")
|
||||
|
||||
// C typedefed unsigned char array.
|
||||
v6, v6l, v6c := testdata.GetCgoTypdefedUnsignedCharArray()
|
||||
v6Len := fmt.Sprintf("%d", v6l)
|
||||
v6Cap := fmt.Sprintf("%d", v6c)
|
||||
v6t := "[6]testdata._Ctype_custom_uchar_t"
|
||||
v6s := "(len=" + v6Len + " cap=" + v6Cap + ") " +
|
||||
"{\n 00000000 74 65 73 74 36 00 " +
|
||||
" |test6.|\n}"
|
||||
addDumpTest(v6, "("+v6t+") "+v6s+"\n")
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
// Copyright (c) 2013 Dave Collins <dave@davec.name>
|
||||
//
|
||||
// Permission to use, copy, modify, and distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
// NOTE: Due to the following build constraints, this file will only be compiled
|
||||
// when either cgo is not supported or "-tags testcgo" is not added to the go
|
||||
// test command line. This file intentionally does not setup any cgo tests in
|
||||
// this scenario.
|
||||
// +build !cgo !testcgo
|
||||
|
||||
package spew_test
|
||||
|
||||
func addCgoDumpTests() {
|
||||
// Don't add any tests for cgo since this file is only compiled when
|
||||
// there should not be any cgo tests.
|
||||
}
|
||||
-226
@@ -1,226 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2013 Dave Collins <dave@davec.name>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
package spew_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
)
|
||||
|
||||
type Flag int
|
||||
|
||||
const (
|
||||
flagOne Flag = iota
|
||||
flagTwo
|
||||
)
|
||||
|
||||
var flagStrings = map[Flag]string{
|
||||
flagOne: "flagOne",
|
||||
flagTwo: "flagTwo",
|
||||
}
|
||||
|
||||
func (f Flag) String() string {
|
||||
if s, ok := flagStrings[f]; ok {
|
||||
return s
|
||||
}
|
||||
return fmt.Sprintf("Unknown flag (%d)", int(f))
|
||||
}
|
||||
|
||||
type Bar struct {
|
||||
data uintptr
|
||||
}
|
||||
|
||||
type Foo struct {
|
||||
unexportedField Bar
|
||||
ExportedField map[interface{}]interface{}
|
||||
}
|
||||
|
||||
// This example demonstrates how to use Dump to dump variables to stdout.
|
||||
func ExampleDump() {
|
||||
// The following package level declarations are assumed for this example:
|
||||
/*
|
||||
type Flag int
|
||||
|
||||
const (
|
||||
flagOne Flag = iota
|
||||
flagTwo
|
||||
)
|
||||
|
||||
var flagStrings = map[Flag]string{
|
||||
flagOne: "flagOne",
|
||||
flagTwo: "flagTwo",
|
||||
}
|
||||
|
||||
func (f Flag) String() string {
|
||||
if s, ok := flagStrings[f]; ok {
|
||||
return s
|
||||
}
|
||||
return fmt.Sprintf("Unknown flag (%d)", int(f))
|
||||
}
|
||||
|
||||
type Bar struct {
|
||||
data uintptr
|
||||
}
|
||||
|
||||
type Foo struct {
|
||||
unexportedField Bar
|
||||
ExportedField map[interface{}]interface{}
|
||||
}
|
||||
*/
|
||||
|
||||
// Setup some sample data structures for the example.
|
||||
bar := Bar{uintptr(0)}
|
||||
s1 := Foo{bar, map[interface{}]interface{}{"one": true}}
|
||||
f := Flag(5)
|
||||
b := []byte{
|
||||
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
|
||||
0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
|
||||
0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
|
||||
0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30,
|
||||
0x31, 0x32,
|
||||
}
|
||||
|
||||
// Dump!
|
||||
spew.Dump(s1, f, b)
|
||||
|
||||
// Output:
|
||||
// (spew_test.Foo) {
|
||||
// unexportedField: (spew_test.Bar) {
|
||||
// data: (uintptr) <nil>
|
||||
// },
|
||||
// ExportedField: (map[interface {}]interface {}) (len=1) {
|
||||
// (string) (len=3) "one": (bool) true
|
||||
// }
|
||||
// }
|
||||
// (spew_test.Flag) Unknown flag (5)
|
||||
// ([]uint8) (len=34 cap=34) {
|
||||
// 00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... |
|
||||
// 00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0|
|
||||
// 00000020 31 32 |12|
|
||||
// }
|
||||
//
|
||||
}
|
||||
|
||||
// This example demonstrates how to use Printf to display a variable with a
|
||||
// format string and inline formatting.
|
||||
func ExamplePrintf() {
|
||||
// Create a double pointer to a uint 8.
|
||||
ui8 := uint8(5)
|
||||
pui8 := &ui8
|
||||
ppui8 := &pui8
|
||||
|
||||
// Create a circular data type.
|
||||
type circular struct {
|
||||
ui8 uint8
|
||||
c *circular
|
||||
}
|
||||
c := circular{ui8: 1}
|
||||
c.c = &c
|
||||
|
||||
// Print!
|
||||
spew.Printf("ppui8: %v\n", ppui8)
|
||||
spew.Printf("circular: %v\n", c)
|
||||
|
||||
// Output:
|
||||
// ppui8: <**>5
|
||||
// circular: {1 <*>{1 <*><shown>}}
|
||||
}
|
||||
|
||||
// This example demonstrates how to use a ConfigState.
|
||||
func ExampleConfigState() {
|
||||
// Modify the indent level of the ConfigState only. The global
|
||||
// configuration is not modified.
|
||||
scs := spew.ConfigState{Indent: "\t"}
|
||||
|
||||
// Output using the ConfigState instance.
|
||||
v := map[string]int{"one": 1}
|
||||
scs.Printf("v: %v\n", v)
|
||||
scs.Dump(v)
|
||||
|
||||
// Output:
|
||||
// v: map[one:1]
|
||||
// (map[string]int) (len=1) {
|
||||
// (string) (len=3) "one": (int) 1
|
||||
// }
|
||||
}
|
||||
|
||||
// This example demonstrates how to use ConfigState.Dump to dump variables to
|
||||
// stdout
|
||||
func ExampleConfigState_Dump() {
|
||||
// See the top-level Dump example for details on the types used in this
|
||||
// example.
|
||||
|
||||
// Create two ConfigState instances with different indentation.
|
||||
scs := spew.ConfigState{Indent: "\t"}
|
||||
scs2 := spew.ConfigState{Indent: " "}
|
||||
|
||||
// Setup some sample data structures for the example.
|
||||
bar := Bar{uintptr(0)}
|
||||
s1 := Foo{bar, map[interface{}]interface{}{"one": true}}
|
||||
|
||||
// Dump using the ConfigState instances.
|
||||
scs.Dump(s1)
|
||||
scs2.Dump(s1)
|
||||
|
||||
// Output:
|
||||
// (spew_test.Foo) {
|
||||
// unexportedField: (spew_test.Bar) {
|
||||
// data: (uintptr) <nil>
|
||||
// },
|
||||
// ExportedField: (map[interface {}]interface {}) (len=1) {
|
||||
// (string) (len=3) "one": (bool) true
|
||||
// }
|
||||
// }
|
||||
// (spew_test.Foo) {
|
||||
// unexportedField: (spew_test.Bar) {
|
||||
// data: (uintptr) <nil>
|
||||
// },
|
||||
// ExportedField: (map[interface {}]interface {}) (len=1) {
|
||||
// (string) (len=3) "one": (bool) true
|
||||
// }
|
||||
// }
|
||||
//
|
||||
}
|
||||
|
||||
// This example demonstrates how to use ConfigState.Printf to display a variable
|
||||
// with a format string and inline formatting.
|
||||
func ExampleConfigState_Printf() {
|
||||
// See the top-level Dump example for details on the types used in this
|
||||
// example.
|
||||
|
||||
// Create two ConfigState instances and modify the method handling of the
|
||||
// first ConfigState only.
|
||||
scs := spew.NewDefaultConfig()
|
||||
scs2 := spew.NewDefaultConfig()
|
||||
scs.DisableMethods = true
|
||||
|
||||
// Alternatively
|
||||
// scs := spew.ConfigState{Indent: " ", DisableMethods: true}
|
||||
// scs2 := spew.ConfigState{Indent: " "}
|
||||
|
||||
// This is of type Flag which implements a Stringer and has raw value 1.
|
||||
f := flagTwo
|
||||
|
||||
// Dump using the ConfigState instances.
|
||||
scs.Printf("f: %v\n", f)
|
||||
scs2.Printf("f: %v\n", f)
|
||||
|
||||
// Output:
|
||||
// f: 1
|
||||
// f: flagTwo
|
||||
}
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2013 Dave Collins <dave@davec.name>
|
||||
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
@@ -182,10 +182,10 @@ func (f *formatState) formatPtr(v reflect.Value) {
|
||||
|
||||
// Display dereferenced value.
|
||||
switch {
|
||||
case nilFound == true:
|
||||
case nilFound:
|
||||
f.fs.Write(nilAngleBytes)
|
||||
|
||||
case cycleFound == true:
|
||||
case cycleFound:
|
||||
f.fs.Write(circularShortBytes)
|
||||
|
||||
default:
|
||||
|
||||
-1558
File diff suppressed because it is too large
Load Diff
-87
@@ -1,87 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2013 Dave Collins <dave@davec.name>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
/*
|
||||
This test file is part of the spew package rather than than the spew_test
|
||||
package because it needs access to internals to properly test certain cases
|
||||
which are not possible via the public interface since they should never happen.
|
||||
*/
|
||||
|
||||
package spew
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// dummyFmtState implements a fake fmt.State to use for testing invalid
|
||||
// reflect.Value handling. This is necessary because the fmt package catches
|
||||
// invalid values before invoking the formatter on them.
|
||||
type dummyFmtState struct {
|
||||
bytes.Buffer
|
||||
}
|
||||
|
||||
func (dfs *dummyFmtState) Flag(f int) bool {
|
||||
if f == int('+') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (dfs *dummyFmtState) Precision() (int, bool) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (dfs *dummyFmtState) Width() (int, bool) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// TestInvalidReflectValue ensures the dump and formatter code handles an
|
||||
// invalid reflect value properly. This needs access to internal state since it
|
||||
// should never happen in real code and therefore can't be tested via the public
|
||||
// API.
|
||||
func TestInvalidReflectValue(t *testing.T) {
|
||||
i := 1
|
||||
|
||||
// Dump invalid reflect value.
|
||||
v := new(reflect.Value)
|
||||
buf := new(bytes.Buffer)
|
||||
d := dumpState{w: buf, cs: &Config}
|
||||
d.dump(*v)
|
||||
s := buf.String()
|
||||
want := "<invalid>"
|
||||
if s != want {
|
||||
t.Errorf("InvalidReflectValue #%d\n got: %s want: %s", i, s, want)
|
||||
}
|
||||
i++
|
||||
|
||||
// Formatter invalid reflect value.
|
||||
buf2 := new(dummyFmtState)
|
||||
f := formatState{value: *v, cs: &Config, fs: buf2}
|
||||
f.format(*v)
|
||||
s = buf2.String()
|
||||
want = "<invalid>"
|
||||
if s != want {
|
||||
t.Errorf("InvalidReflectValue #%d got: %s want: %s", i, s, want)
|
||||
}
|
||||
}
|
||||
|
||||
// SortValues makes the internal sortValues function available to the test
|
||||
// package.
|
||||
func SortValues(values []reflect.Value, cs *ConfigState) {
|
||||
sortValues(values, cs)
|
||||
}
|
||||
-101
@@ -1,101 +0,0 @@
|
||||
// Copyright (c) 2013-2015 Dave Collins <dave@davec.name>
|
||||
|
||||
// Permission to use, copy, modify, and distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
// NOTE: Due to the following build constraints, this file will only be compiled
|
||||
// when the code is not running on Google App Engine and "-tags disableunsafe"
|
||||
// is not added to the go build command line.
|
||||
// +build !appengine,!disableunsafe
|
||||
|
||||
/*
|
||||
This test file is part of the spew package rather than than the spew_test
|
||||
package because it needs access to internals to properly test certain cases
|
||||
which are not possible via the public interface since they should never happen.
|
||||
*/
|
||||
|
||||
package spew
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// changeKind uses unsafe to intentionally change the kind of a reflect.Value to
|
||||
// the maximum kind value which does not exist. This is needed to test the
|
||||
// fallback code which punts to the standard fmt library for new types that
|
||||
// might get added to the language.
|
||||
func changeKind(v *reflect.Value, readOnly bool) {
|
||||
rvf := (*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + offsetFlag))
|
||||
*rvf = *rvf | ((1<<flagKindWidth - 1) << flagKindShift)
|
||||
if readOnly {
|
||||
*rvf |= flagRO
|
||||
} else {
|
||||
*rvf &= ^uintptr(flagRO)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAddedReflectValue tests functionaly of the dump and formatter code which
|
||||
// falls back to the standard fmt library for new types that might get added to
|
||||
// the language.
|
||||
func TestAddedReflectValue(t *testing.T) {
|
||||
i := 1
|
||||
|
||||
// Dump using a reflect.Value that is exported.
|
||||
v := reflect.ValueOf(int8(5))
|
||||
changeKind(&v, false)
|
||||
buf := new(bytes.Buffer)
|
||||
d := dumpState{w: buf, cs: &Config}
|
||||
d.dump(v)
|
||||
s := buf.String()
|
||||
want := "(int8) 5"
|
||||
if s != want {
|
||||
t.Errorf("TestAddedReflectValue #%d\n got: %s want: %s", i, s, want)
|
||||
}
|
||||
i++
|
||||
|
||||
// Dump using a reflect.Value that is not exported.
|
||||
changeKind(&v, true)
|
||||
buf.Reset()
|
||||
d.dump(v)
|
||||
s = buf.String()
|
||||
want = "(int8) <int8 Value>"
|
||||
if s != want {
|
||||
t.Errorf("TestAddedReflectValue #%d\n got: %s want: %s", i, s, want)
|
||||
}
|
||||
i++
|
||||
|
||||
// Formatter using a reflect.Value that is exported.
|
||||
changeKind(&v, false)
|
||||
buf2 := new(dummyFmtState)
|
||||
f := formatState{value: v, cs: &Config, fs: buf2}
|
||||
f.format(v)
|
||||
s = buf2.String()
|
||||
want = "5"
|
||||
if s != want {
|
||||
t.Errorf("TestAddedReflectValue #%d got: %s want: %s", i, s, want)
|
||||
}
|
||||
i++
|
||||
|
||||
// Formatter using a reflect.Value that is not exported.
|
||||
changeKind(&v, true)
|
||||
buf2.Reset()
|
||||
f = formatState{value: v, cs: &Config, fs: buf2}
|
||||
f.format(v)
|
||||
s = buf2.String()
|
||||
want = "<int8 Value>"
|
||||
if s != want {
|
||||
t.Errorf("TestAddedReflectValue #%d got: %s want: %s", i, s, want)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2013 Dave Collins <dave@davec.name>
|
||||
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
|
||||
-309
@@ -1,309 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2013 Dave Collins <dave@davec.name>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
package spew_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
)
|
||||
|
||||
// spewFunc is used to identify which public function of the spew package or
|
||||
// ConfigState a test applies to.
|
||||
type spewFunc int
|
||||
|
||||
const (
|
||||
fCSFdump spewFunc = iota
|
||||
fCSFprint
|
||||
fCSFprintf
|
||||
fCSFprintln
|
||||
fCSPrint
|
||||
fCSPrintln
|
||||
fCSSdump
|
||||
fCSSprint
|
||||
fCSSprintf
|
||||
fCSSprintln
|
||||
fCSErrorf
|
||||
fCSNewFormatter
|
||||
fErrorf
|
||||
fFprint
|
||||
fFprintln
|
||||
fPrint
|
||||
fPrintln
|
||||
fSdump
|
||||
fSprint
|
||||
fSprintf
|
||||
fSprintln
|
||||
)
|
||||
|
||||
// Map of spewFunc values to names for pretty printing.
|
||||
var spewFuncStrings = map[spewFunc]string{
|
||||
fCSFdump: "ConfigState.Fdump",
|
||||
fCSFprint: "ConfigState.Fprint",
|
||||
fCSFprintf: "ConfigState.Fprintf",
|
||||
fCSFprintln: "ConfigState.Fprintln",
|
||||
fCSSdump: "ConfigState.Sdump",
|
||||
fCSPrint: "ConfigState.Print",
|
||||
fCSPrintln: "ConfigState.Println",
|
||||
fCSSprint: "ConfigState.Sprint",
|
||||
fCSSprintf: "ConfigState.Sprintf",
|
||||
fCSSprintln: "ConfigState.Sprintln",
|
||||
fCSErrorf: "ConfigState.Errorf",
|
||||
fCSNewFormatter: "ConfigState.NewFormatter",
|
||||
fErrorf: "spew.Errorf",
|
||||
fFprint: "spew.Fprint",
|
||||
fFprintln: "spew.Fprintln",
|
||||
fPrint: "spew.Print",
|
||||
fPrintln: "spew.Println",
|
||||
fSdump: "spew.Sdump",
|
||||
fSprint: "spew.Sprint",
|
||||
fSprintf: "spew.Sprintf",
|
||||
fSprintln: "spew.Sprintln",
|
||||
}
|
||||
|
||||
func (f spewFunc) String() string {
|
||||
if s, ok := spewFuncStrings[f]; ok {
|
||||
return s
|
||||
}
|
||||
return fmt.Sprintf("Unknown spewFunc (%d)", int(f))
|
||||
}
|
||||
|
||||
// spewTest is used to describe a test to be performed against the public
|
||||
// functions of the spew package or ConfigState.
|
||||
type spewTest struct {
|
||||
cs *spew.ConfigState
|
||||
f spewFunc
|
||||
format string
|
||||
in interface{}
|
||||
want string
|
||||
}
|
||||
|
||||
// spewTests houses the tests to be performed against the public functions of
|
||||
// the spew package and ConfigState.
|
||||
//
|
||||
// These tests are only intended to ensure the public functions are exercised
|
||||
// and are intentionally not exhaustive of types. The exhaustive type
|
||||
// tests are handled in the dump and format tests.
|
||||
var spewTests []spewTest
|
||||
|
||||
// redirStdout is a helper function to return the standard output from f as a
|
||||
// byte slice.
|
||||
func redirStdout(f func()) ([]byte, error) {
|
||||
tempFile, err := ioutil.TempFile("", "ss-test")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fileName := tempFile.Name()
|
||||
defer os.Remove(fileName) // Ignore error
|
||||
|
||||
origStdout := os.Stdout
|
||||
os.Stdout = tempFile
|
||||
f()
|
||||
os.Stdout = origStdout
|
||||
tempFile.Close()
|
||||
|
||||
return ioutil.ReadFile(fileName)
|
||||
}
|
||||
|
||||
func initSpewTests() {
|
||||
// Config states with various settings.
|
||||
scsDefault := spew.NewDefaultConfig()
|
||||
scsNoMethods := &spew.ConfigState{Indent: " ", DisableMethods: true}
|
||||
scsNoPmethods := &spew.ConfigState{Indent: " ", DisablePointerMethods: true}
|
||||
scsMaxDepth := &spew.ConfigState{Indent: " ", MaxDepth: 1}
|
||||
scsContinue := &spew.ConfigState{Indent: " ", ContinueOnMethod: true}
|
||||
|
||||
// Variables for tests on types which implement Stringer interface with and
|
||||
// without a pointer receiver.
|
||||
ts := stringer("test")
|
||||
tps := pstringer("test")
|
||||
|
||||
// depthTester is used to test max depth handling for structs, array, slices
|
||||
// and maps.
|
||||
type depthTester struct {
|
||||
ic indirCir1
|
||||
arr [1]string
|
||||
slice []string
|
||||
m map[string]int
|
||||
}
|
||||
dt := depthTester{indirCir1{nil}, [1]string{"arr"}, []string{"slice"},
|
||||
map[string]int{"one": 1}}
|
||||
|
||||
// Variable for tests on types which implement error interface.
|
||||
te := customError(10)
|
||||
|
||||
spewTests = []spewTest{
|
||||
{scsDefault, fCSFdump, "", int8(127), "(int8) 127\n"},
|
||||
{scsDefault, fCSFprint, "", int16(32767), "32767"},
|
||||
{scsDefault, fCSFprintf, "%v", int32(2147483647), "2147483647"},
|
||||
{scsDefault, fCSFprintln, "", int(2147483647), "2147483647\n"},
|
||||
{scsDefault, fCSPrint, "", int64(9223372036854775807), "9223372036854775807"},
|
||||
{scsDefault, fCSPrintln, "", uint8(255), "255\n"},
|
||||
{scsDefault, fCSSdump, "", uint8(64), "(uint8) 64\n"},
|
||||
{scsDefault, fCSSprint, "", complex(1, 2), "(1+2i)"},
|
||||
{scsDefault, fCSSprintf, "%v", complex(float32(3), 4), "(3+4i)"},
|
||||
{scsDefault, fCSSprintln, "", complex(float64(5), 6), "(5+6i)\n"},
|
||||
{scsDefault, fCSErrorf, "%#v", uint16(65535), "(uint16)65535"},
|
||||
{scsDefault, fCSNewFormatter, "%v", uint32(4294967295), "4294967295"},
|
||||
{scsDefault, fErrorf, "%v", uint64(18446744073709551615), "18446744073709551615"},
|
||||
{scsDefault, fFprint, "", float32(3.14), "3.14"},
|
||||
{scsDefault, fFprintln, "", float64(6.28), "6.28\n"},
|
||||
{scsDefault, fPrint, "", true, "true"},
|
||||
{scsDefault, fPrintln, "", false, "false\n"},
|
||||
{scsDefault, fSdump, "", complex(-10, -20), "(complex128) (-10-20i)\n"},
|
||||
{scsDefault, fSprint, "", complex(-1, -2), "(-1-2i)"},
|
||||
{scsDefault, fSprintf, "%v", complex(float32(-3), -4), "(-3-4i)"},
|
||||
{scsDefault, fSprintln, "", complex(float64(-5), -6), "(-5-6i)\n"},
|
||||
{scsNoMethods, fCSFprint, "", ts, "test"},
|
||||
{scsNoMethods, fCSFprint, "", &ts, "<*>test"},
|
||||
{scsNoMethods, fCSFprint, "", tps, "test"},
|
||||
{scsNoMethods, fCSFprint, "", &tps, "<*>test"},
|
||||
{scsNoPmethods, fCSFprint, "", ts, "stringer test"},
|
||||
{scsNoPmethods, fCSFprint, "", &ts, "<*>stringer test"},
|
||||
{scsNoPmethods, fCSFprint, "", tps, "test"},
|
||||
{scsNoPmethods, fCSFprint, "", &tps, "<*>stringer test"},
|
||||
{scsMaxDepth, fCSFprint, "", dt, "{{<max>} [<max>] [<max>] map[<max>]}"},
|
||||
{scsMaxDepth, fCSFdump, "", dt, "(spew_test.depthTester) {\n" +
|
||||
" ic: (spew_test.indirCir1) {\n <max depth reached>\n },\n" +
|
||||
" arr: ([1]string) (len=1 cap=1) {\n <max depth reached>\n },\n" +
|
||||
" slice: ([]string) (len=1 cap=1) {\n <max depth reached>\n },\n" +
|
||||
" m: (map[string]int) (len=1) {\n <max depth reached>\n }\n}\n"},
|
||||
{scsContinue, fCSFprint, "", ts, "(stringer test) test"},
|
||||
{scsContinue, fCSFdump, "", ts, "(spew_test.stringer) " +
|
||||
"(len=4) (stringer test) \"test\"\n"},
|
||||
{scsContinue, fCSFprint, "", te, "(error: 10) 10"},
|
||||
{scsContinue, fCSFdump, "", te, "(spew_test.customError) " +
|
||||
"(error: 10) 10\n"},
|
||||
}
|
||||
}
|
||||
|
||||
// TestSpew executes all of the tests described by spewTests.
|
||||
func TestSpew(t *testing.T) {
|
||||
initSpewTests()
|
||||
|
||||
t.Logf("Running %d tests", len(spewTests))
|
||||
for i, test := range spewTests {
|
||||
buf := new(bytes.Buffer)
|
||||
switch test.f {
|
||||
case fCSFdump:
|
||||
test.cs.Fdump(buf, test.in)
|
||||
|
||||
case fCSFprint:
|
||||
test.cs.Fprint(buf, test.in)
|
||||
|
||||
case fCSFprintf:
|
||||
test.cs.Fprintf(buf, test.format, test.in)
|
||||
|
||||
case fCSFprintln:
|
||||
test.cs.Fprintln(buf, test.in)
|
||||
|
||||
case fCSPrint:
|
||||
b, err := redirStdout(func() { test.cs.Print(test.in) })
|
||||
if err != nil {
|
||||
t.Errorf("%v #%d %v", test.f, i, err)
|
||||
continue
|
||||
}
|
||||
buf.Write(b)
|
||||
|
||||
case fCSPrintln:
|
||||
b, err := redirStdout(func() { test.cs.Println(test.in) })
|
||||
if err != nil {
|
||||
t.Errorf("%v #%d %v", test.f, i, err)
|
||||
continue
|
||||
}
|
||||
buf.Write(b)
|
||||
|
||||
case fCSSdump:
|
||||
str := test.cs.Sdump(test.in)
|
||||
buf.WriteString(str)
|
||||
|
||||
case fCSSprint:
|
||||
str := test.cs.Sprint(test.in)
|
||||
buf.WriteString(str)
|
||||
|
||||
case fCSSprintf:
|
||||
str := test.cs.Sprintf(test.format, test.in)
|
||||
buf.WriteString(str)
|
||||
|
||||
case fCSSprintln:
|
||||
str := test.cs.Sprintln(test.in)
|
||||
buf.WriteString(str)
|
||||
|
||||
case fCSErrorf:
|
||||
err := test.cs.Errorf(test.format, test.in)
|
||||
buf.WriteString(err.Error())
|
||||
|
||||
case fCSNewFormatter:
|
||||
fmt.Fprintf(buf, test.format, test.cs.NewFormatter(test.in))
|
||||
|
||||
case fErrorf:
|
||||
err := spew.Errorf(test.format, test.in)
|
||||
buf.WriteString(err.Error())
|
||||
|
||||
case fFprint:
|
||||
spew.Fprint(buf, test.in)
|
||||
|
||||
case fFprintln:
|
||||
spew.Fprintln(buf, test.in)
|
||||
|
||||
case fPrint:
|
||||
b, err := redirStdout(func() { spew.Print(test.in) })
|
||||
if err != nil {
|
||||
t.Errorf("%v #%d %v", test.f, i, err)
|
||||
continue
|
||||
}
|
||||
buf.Write(b)
|
||||
|
||||
case fPrintln:
|
||||
b, err := redirStdout(func() { spew.Println(test.in) })
|
||||
if err != nil {
|
||||
t.Errorf("%v #%d %v", test.f, i, err)
|
||||
continue
|
||||
}
|
||||
buf.Write(b)
|
||||
|
||||
case fSdump:
|
||||
str := spew.Sdump(test.in)
|
||||
buf.WriteString(str)
|
||||
|
||||
case fSprint:
|
||||
str := spew.Sprint(test.in)
|
||||
buf.WriteString(str)
|
||||
|
||||
case fSprintf:
|
||||
str := spew.Sprintf(test.format, test.in)
|
||||
buf.WriteString(str)
|
||||
|
||||
case fSprintln:
|
||||
str := spew.Sprintln(test.in)
|
||||
buf.WriteString(str)
|
||||
|
||||
default:
|
||||
t.Errorf("%v #%d unrecognized function", test.f, i)
|
||||
continue
|
||||
}
|
||||
s := buf.String()
|
||||
if test.want != s {
|
||||
t.Errorf("ConfigState #%d\n got: %s want: %s", i, s, test.want)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
language: go
|
||||
sudo: false
|
||||
go:
|
||||
- 1.6.4
|
||||
- 1.7.4
|
||||
- tip
|
||||
|
||||
git:
|
||||
depth: 3
|
||||
|
||||
script:
|
||||
- go test -v -covermode=count -coverprofile=coverage.out
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Manuel Martínez-Almeida
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
# Server-Sent Events
|
||||
|
||||
[](https://godoc.org/github.com/gin-contrib/sse)
|
||||
[](https://travis-ci.org/gin-contrib/sse)
|
||||
[](https://codecov.io/gh/gin-contrib/sse)
|
||||
[](https://goreportcard.com/report/github.com/gin-contrib/sse)
|
||||
|
||||
Server-sent events (SSE) is a technology where a browser receives automatic updates from a server via HTTP connection. The Server-Sent Events EventSource API is [standardized as part of HTML5[1] by the W3C](http://www.w3.org/TR/2009/WD-eventsource-20091029/).
|
||||
|
||||
- [Read this great SSE introduction by the HTML5Rocks guys](http://www.html5rocks.com/en/tutorials/eventsource/basics/)
|
||||
- [Browser support](http://caniuse.com/#feat=eventsource)
|
||||
|
||||
## Sample code
|
||||
|
||||
```go
|
||||
import "github.com/gin-contrib/sse"
|
||||
|
||||
func httpHandler(w http.ResponseWriter, req *http.Request) {
|
||||
// data can be a primitive like a string, an integer or a float
|
||||
sse.Encode(w, sse.Event{
|
||||
Event: "message",
|
||||
Data: "some data\nmore data",
|
||||
})
|
||||
|
||||
// also a complex type, like a map, a struct or a slice
|
||||
sse.Encode(w, sse.Event{
|
||||
Id: "124",
|
||||
Event: "message",
|
||||
Data: map[string]interface{}{
|
||||
"user": "manu",
|
||||
"date": time.Now().Unix(),
|
||||
"content": "hi!",
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
```
|
||||
event: message
|
||||
data: some data\\nmore data
|
||||
|
||||
id: 124
|
||||
event: message
|
||||
data: {"content":"hi!","date":1431540810,"user":"manu"}
|
||||
|
||||
```
|
||||
|
||||
## Content-Type
|
||||
|
||||
```go
|
||||
fmt.Println(sse.ContentType)
|
||||
```
|
||||
```
|
||||
text/event-stream
|
||||
```
|
||||
|
||||
## Decoding support
|
||||
|
||||
There is a client-side implementation of SSE coming soon.
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package sse
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
type decoder struct {
|
||||
events []Event
|
||||
}
|
||||
|
||||
func Decode(r io.Reader) ([]Event, error) {
|
||||
var dec decoder
|
||||
return dec.decode(r)
|
||||
}
|
||||
|
||||
func (d *decoder) dispatchEvent(event Event, data string) {
|
||||
dataLength := len(data)
|
||||
if dataLength > 0 {
|
||||
//If the data buffer's last character is a U+000A LINE FEED (LF) character, then remove the last character from the data buffer.
|
||||
data = data[:dataLength-1]
|
||||
dataLength--
|
||||
}
|
||||
if dataLength == 0 && event.Event == "" {
|
||||
return
|
||||
}
|
||||
if event.Event == "" {
|
||||
event.Event = "message"
|
||||
}
|
||||
event.Data = data
|
||||
d.events = append(d.events, event)
|
||||
}
|
||||
|
||||
func (d *decoder) decode(r io.Reader) ([]Event, error) {
|
||||
buf, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var currentEvent Event
|
||||
var dataBuffer *bytes.Buffer = new(bytes.Buffer)
|
||||
// TODO (and unit tests)
|
||||
// Lines must be separated by either a U+000D CARRIAGE RETURN U+000A LINE FEED (CRLF) character pair,
|
||||
// a single U+000A LINE FEED (LF) character,
|
||||
// or a single U+000D CARRIAGE RETURN (CR) character.
|
||||
lines := bytes.Split(buf, []byte{'\n'})
|
||||
for _, line := range lines {
|
||||
if len(line) == 0 {
|
||||
// If the line is empty (a blank line). Dispatch the event.
|
||||
d.dispatchEvent(currentEvent, dataBuffer.String())
|
||||
|
||||
// reset current event and data buffer
|
||||
currentEvent = Event{}
|
||||
dataBuffer.Reset()
|
||||
continue
|
||||
}
|
||||
if line[0] == byte(':') {
|
||||
// If the line starts with a U+003A COLON character (:), ignore the line.
|
||||
continue
|
||||
}
|
||||
|
||||
var field, value []byte
|
||||
colonIndex := bytes.IndexRune(line, ':')
|
||||
if colonIndex != -1 {
|
||||
// If the line contains a U+003A COLON character character (:)
|
||||
// Collect the characters on the line before the first U+003A COLON character (:),
|
||||
// and let field be that string.
|
||||
field = line[:colonIndex]
|
||||
// Collect the characters on the line after the first U+003A COLON character (:),
|
||||
// and let value be that string.
|
||||
value = line[colonIndex+1:]
|
||||
// If value starts with a single U+0020 SPACE character, remove it from value.
|
||||
if len(value) > 0 && value[0] == ' ' {
|
||||
value = value[1:]
|
||||
}
|
||||
} else {
|
||||
// Otherwise, the string is not empty but does not contain a U+003A COLON character character (:)
|
||||
// Use the whole line as the field name, and the empty string as the field value.
|
||||
field = line
|
||||
value = []byte{}
|
||||
}
|
||||
// The steps to process the field given a field name and a field value depend on the field name,
|
||||
// as given in the following list. Field names must be compared literally,
|
||||
// with no case folding performed.
|
||||
switch string(field) {
|
||||
case "event":
|
||||
// Set the event name buffer to field value.
|
||||
currentEvent.Event = string(value)
|
||||
case "id":
|
||||
// Set the event stream's last event ID to the field value.
|
||||
currentEvent.Id = string(value)
|
||||
case "retry":
|
||||
// If the field value consists of only characters in the range U+0030 DIGIT ZERO (0) to U+0039 DIGIT NINE (9),
|
||||
// then interpret the field value as an integer in base ten, and set the event stream's reconnection time to that integer.
|
||||
// Otherwise, ignore the field.
|
||||
currentEvent.Id = string(value)
|
||||
case "data":
|
||||
// Append the field value to the data buffer,
|
||||
dataBuffer.Write(value)
|
||||
// then append a single U+000A LINE FEED (LF) character to the data buffer.
|
||||
dataBuffer.WriteString("\n")
|
||||
default:
|
||||
//Otherwise. The field is ignored.
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Once the end of the file is reached, the user agent must dispatch the event one final time.
|
||||
d.dispatchEvent(currentEvent, dataBuffer.String())
|
||||
|
||||
return d.events, nil
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package sse
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Server-Sent Events
|
||||
// W3C Working Draft 29 October 2009
|
||||
// http://www.w3.org/TR/2009/WD-eventsource-20091029/
|
||||
|
||||
const ContentType = "text/event-stream"
|
||||
|
||||
var contentType = []string{ContentType}
|
||||
var noCache = []string{"no-cache"}
|
||||
|
||||
var fieldReplacer = strings.NewReplacer(
|
||||
"\n", "\\n",
|
||||
"\r", "\\r")
|
||||
|
||||
var dataReplacer = strings.NewReplacer(
|
||||
"\n", "\ndata:",
|
||||
"\r", "\\r")
|
||||
|
||||
type Event struct {
|
||||
Event string
|
||||
Id string
|
||||
Retry uint
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
func Encode(writer io.Writer, event Event) error {
|
||||
w := checkWriter(writer)
|
||||
writeId(w, event.Id)
|
||||
writeEvent(w, event.Event)
|
||||
writeRetry(w, event.Retry)
|
||||
return writeData(w, event.Data)
|
||||
}
|
||||
|
||||
func writeId(w stringWriter, id string) {
|
||||
if len(id) > 0 {
|
||||
w.WriteString("id:")
|
||||
fieldReplacer.WriteString(w, id)
|
||||
w.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
func writeEvent(w stringWriter, event string) {
|
||||
if len(event) > 0 {
|
||||
w.WriteString("event:")
|
||||
fieldReplacer.WriteString(w, event)
|
||||
w.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
func writeRetry(w stringWriter, retry uint) {
|
||||
if retry > 0 {
|
||||
w.WriteString("retry:")
|
||||
w.WriteString(strconv.FormatUint(uint64(retry), 10))
|
||||
w.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
func writeData(w stringWriter, data interface{}) error {
|
||||
w.WriteString("data:")
|
||||
switch kindOfData(data) {
|
||||
case reflect.Struct, reflect.Slice, reflect.Map:
|
||||
err := json.NewEncoder(w).Encode(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.WriteString("\n")
|
||||
default:
|
||||
dataReplacer.WriteString(w, fmt.Sprint(data))
|
||||
w.WriteString("\n\n")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r Event) Render(w http.ResponseWriter) error {
|
||||
r.WriteContentType(w)
|
||||
return Encode(w, r)
|
||||
}
|
||||
|
||||
func (r Event) WriteContentType(w http.ResponseWriter) {
|
||||
header := w.Header()
|
||||
header["Content-Type"] = contentType
|
||||
|
||||
if _, exist := header["Cache-Control"]; !exist {
|
||||
header["Cache-Control"] = noCache
|
||||
}
|
||||
}
|
||||
|
||||
func kindOfData(data interface{}) reflect.Kind {
|
||||
value := reflect.ValueOf(data)
|
||||
valueType := value.Kind()
|
||||
if valueType == reflect.Ptr {
|
||||
valueType = value.Elem().Kind()
|
||||
}
|
||||
return valueType
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package sse
|
||||
|
||||
import "io"
|
||||
|
||||
type stringWriter interface {
|
||||
io.Writer
|
||||
WriteString(string) (int, error)
|
||||
}
|
||||
|
||||
type stringWrapper struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (w stringWrapper) WriteString(str string) (int, error) {
|
||||
return w.Writer.Write([]byte(str))
|
||||
}
|
||||
|
||||
func checkWriter(writer io.Writer) stringWriter {
|
||||
if w, ok := writer.(stringWriter); ok {
|
||||
return w
|
||||
} else {
|
||||
return stringWrapper{writer}
|
||||
}
|
||||
}
|
||||
+5
-2
@@ -1,2 +1,5 @@
|
||||
Godeps/*
|
||||
!Godeps/Godeps.json
|
||||
vendor/*
|
||||
!vendor/vendor.json
|
||||
coverage.out
|
||||
count.out
|
||||
test
|
||||
|
||||
+32
-3
@@ -1,6 +1,35 @@
|
||||
language: go
|
||||
sudo: false
|
||||
go:
|
||||
- 1.3
|
||||
- 1.4
|
||||
- tip
|
||||
- 1.6.x
|
||||
- 1.7.x
|
||||
- 1.8.x
|
||||
- 1.9.x
|
||||
- 1.10.x
|
||||
- master
|
||||
|
||||
git:
|
||||
depth: 10
|
||||
|
||||
install:
|
||||
- make install
|
||||
|
||||
go_import_path: github.com/gin-gonic/gin
|
||||
|
||||
script:
|
||||
- make vet
|
||||
- make fmt-check
|
||||
- make embedmd
|
||||
- make misspell-check
|
||||
- make test
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
notifications:
|
||||
webhooks:
|
||||
urls:
|
||||
- https://webhooks.gitter.im/e/7f95bf605c4d356372f4
|
||||
on_success: change # options: [always|never|change] default: always
|
||||
on_failure: always # options: [always|never|change] default: always
|
||||
on_start: false # default: false
|
||||
|
||||
+61
-4
@@ -1,14 +1,19 @@
|
||||
List of all the awesome people working to make Gin the best Web Framework in Go.
|
||||
|
||||
## gin 1.x series authors
|
||||
|
||||
**Gin Core Team:** Bo-Yi Wu (@appleboy), 田欧 (@thinkerou), Javier Provecho (@javierprovecho)
|
||||
|
||||
##gin 0.x series authors
|
||||
## gin 0.x series authors
|
||||
|
||||
**Original Developer:** Manu Martinez-Almeida (@manucorporat)
|
||||
**Long-term Maintainer:** Javier Provecho (@javierprovecho)
|
||||
**Maintainers:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho)
|
||||
|
||||
People and companies, who have contributed, in alphabetical order.
|
||||
|
||||
**@858806258 (杰哥)**
|
||||
- Fix typo in example
|
||||
|
||||
|
||||
**@achedeuzot (Klemen Sever)**
|
||||
- Fix newline debug printing
|
||||
|
||||
@@ -21,6 +26,10 @@ People and companies, who have contributed, in alphabetical order.
|
||||
- Typos in README
|
||||
|
||||
|
||||
**@alexanderdidenko (Aleksandr Didenko)**
|
||||
- Add support multipart/form-data
|
||||
|
||||
|
||||
**@alexandernyquist (Alexander Nyquist)**
|
||||
- Using template.Must to fix multiple return issue
|
||||
- ★ Added support for OPTIONS verb
|
||||
@@ -55,15 +64,39 @@ People and companies, who have contributed, in alphabetical order.
|
||||
- Add example about serving static files
|
||||
|
||||
|
||||
**@donileo (Adonis)**
|
||||
- Add NoMethod handler
|
||||
|
||||
|
||||
**@dutchcoders (DutchCoders)**
|
||||
- ★ Fix security bug that allows client to spoof ip
|
||||
- Fix typo. r.HTMLTemplates -> SetHTMLTemplate
|
||||
|
||||
|
||||
**@el3ctro- (Joshua Loper)**
|
||||
- Fix typo in example
|
||||
|
||||
|
||||
**@ethankan (Ethan Kan)**
|
||||
- Unsigned integers in binding
|
||||
|
||||
|
||||
**(Evgeny Persienko)**
|
||||
- Validate sub structures
|
||||
|
||||
|
||||
**@frankbille (Frank Bille)**
|
||||
- Add support for HTTP Realm Auth
|
||||
|
||||
|
||||
**@fmd (Fareed Dudhia)**
|
||||
- Fix typo. SetHTTPTemplate -> SetHTMLTemplate
|
||||
|
||||
|
||||
**@ironiridis (Christopher Harrington)**
|
||||
- Remove old reference
|
||||
|
||||
|
||||
**@jammie-stackhouse (Jamie Stackhouse)**
|
||||
- Add more shortcuts for router methods
|
||||
|
||||
@@ -104,6 +137,10 @@ People and companies, who have contributed, in alphabetical order.
|
||||
- ★ work around path.Join removing trailing slashes from routes
|
||||
|
||||
|
||||
**@mattn (Yasuhiro Matsumoto)**
|
||||
- Improve color logger
|
||||
|
||||
|
||||
**@mdigger (Dmitry Sedykh)**
|
||||
- Fixes Form binding when content-type is x-www-form-urlencoded
|
||||
- No repeat call c.Writer.Status() in gin.Logger
|
||||
@@ -138,10 +175,22 @@ People and companies, who have contributed, in alphabetical order.
|
||||
- Fix Port usage in README.
|
||||
|
||||
|
||||
**@rayrod2030 (Ray Rodriguez)**
|
||||
- Fix typo in example
|
||||
|
||||
|
||||
**@rns**
|
||||
- Fix typo in example
|
||||
|
||||
|
||||
**@RobAWilkinson (Robert Wilkinson)**
|
||||
- Add example of forms and params
|
||||
|
||||
|
||||
**@rogierlommers (Rogier Lommers)**
|
||||
- Add updated static serve example
|
||||
|
||||
|
||||
**@se77en (Damon Zhao)**
|
||||
- Improve color logging
|
||||
|
||||
@@ -166,9 +215,17 @@ People and companies, who have contributed, in alphabetical order.
|
||||
- Update httprouter godeps
|
||||
|
||||
|
||||
**@tebeka (Miki Tebeka)**
|
||||
- Use net/http constants instead of numeric values
|
||||
|
||||
|
||||
**@techjanitor**
|
||||
- Update context.go reserved IPs
|
||||
|
||||
|
||||
**@yosssi (Keiji Yoshida)**
|
||||
- Fix link in README
|
||||
|
||||
|
||||
**@yuyabee**
|
||||
- Fixed README
|
||||
- Fixed README
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user