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
|
sudo: required
|
||||||
|
dist: trusty
|
||||||
|
group: deprecated-2017Q4
|
||||||
|
|
||||||
language: go
|
language: go
|
||||||
|
|
||||||
@@ -6,14 +8,15 @@ services:
|
|||||||
- docker
|
- docker
|
||||||
|
|
||||||
addons:
|
addons:
|
||||||
postgresql: "9.4"
|
postgresql: "9.6"
|
||||||
|
|
||||||
go:
|
go:
|
||||||
- 1.5.4
|
- 1.10.x
|
||||||
- 1.6.2
|
- 1.11.x
|
||||||
|
|
||||||
env:
|
before_install:
|
||||||
- GO15VENDOREXPERIMENT=1
|
- ./script/check_formatting.sh
|
||||||
|
- ./script/check_assets.sh
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- make setup
|
- make setup
|
||||||
@@ -21,4 +24,5 @@ install:
|
|||||||
script:
|
script:
|
||||||
- make build
|
- make build
|
||||||
- make test
|
- 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
|
## 0.9.2 - 2016-03-01
|
||||||
|
|
||||||
- Fixes bug with unsafe base64 encoded sql queries
|
- Fixes bug with unsafe base64 encoded sql queries
|
||||||
|
|||||||
+2
-1
@@ -2,5 +2,6 @@
|
|||||||
- Create a new git branch
|
- Create a new git branch
|
||||||
- Make changes
|
- Make changes
|
||||||
- Run tests: `make test`
|
- 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`
|
- 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
|
FROM alpine:3.6
|
||||||
MAINTAINER Dan Sosedoff <dan.sosedoff@gmail.com>
|
LABEL maintainer="Dan Sosedoff <dan.sosedoff@gmail.com>"
|
||||||
|
ENV PGWEB_VERSION 0.11.1
|
||||||
ENV PGWEB_VERSION 0.9.2
|
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
apk update && \
|
apk update && \
|
||||||
apk add ca-certificates && \
|
apk add --no-cache ca-certificates openssl postgresql wget && \
|
||||||
update-ca-certificates && \
|
update-ca-certificates && \
|
||||||
|
rm -rf /var/cache/apk/* && \
|
||||||
cd /tmp && \
|
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 && \
|
unzip pgweb_linux_amd64.zip -d /usr/bin && \
|
||||||
mv /usr/bin/pgweb_linux_amd64 /usr/bin/pgweb && \
|
mv /usr/bin/pgweb_linux_amd64 /usr/bin/pgweb && \
|
||||||
rm -f pgweb_linux_amd64.zip
|
rm -f pgweb_linux_amd64.zip
|
||||||
|
|
||||||
EXPOSE 8081
|
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)
|
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
|
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
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ usage:
|
|||||||
@echo "----------------- : -------------------"
|
@echo "----------------- : -------------------"
|
||||||
@echo "make setup : Install all necessary dependencies"
|
@echo "make setup : Install all necessary dependencies"
|
||||||
@echo "make dev : Generate development build"
|
@echo "make dev : Generate development build"
|
||||||
@echo "make test : Run tests"
|
|
||||||
@echo "make build : Generate production build for current OS"
|
@echo "make build : Generate production build for current OS"
|
||||||
@echo "make bootstrap : Install cross-compilation toolchain"
|
@echo "make bootstrap : Install cross-compilation toolchain"
|
||||||
@echo "make release : Generate binaries for all supported OSes"
|
@echo "make release : Generate binaries for all supported OSes"
|
||||||
@@ -26,10 +25,11 @@ usage:
|
|||||||
@echo ""
|
@echo ""
|
||||||
|
|
||||||
test:
|
test:
|
||||||
go test -cover ./pkg/...
|
go test -race -cover ./pkg/...
|
||||||
|
|
||||||
test-all:
|
test-all:
|
||||||
@./script/test_all.sh
|
@./script/test_all.sh
|
||||||
|
@./script/test_cockroach.sh
|
||||||
|
|
||||||
assets: static/
|
assets: static/
|
||||||
go-bindata -o pkg/data/bindata.go -pkg data $(BINDATA_OPTS) $(BINDATA_IGNORE) -ignore=[.]gitignore -ignore=[.]gitkeep $<...
|
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..."
|
@echo "Building binaries..."
|
||||||
@gox \
|
@gox \
|
||||||
-osarch "$(TARGETS)" \
|
-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}}"
|
-output "./bin/pgweb_{{.OS}}_{{.Arch}}"
|
||||||
|
|
||||||
@echo "Building ARM binaries..."
|
@echo "Building ARM binaries..."
|
||||||
GOOS=linux GOARCH=arm GOARM=5 go build \
|
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"
|
-o "./bin/pgweb_linux_arm_v5"
|
||||||
|
|
||||||
@echo "\nPackaging binaries...\n"
|
@echo "\nPackaging binaries...\n"
|
||||||
@@ -64,11 +64,11 @@ bootstrap:
|
|||||||
gox -build-toolchain
|
gox -build-toolchain
|
||||||
|
|
||||||
setup:
|
setup:
|
||||||
go get github.com/tools/godep
|
go get -u github.com/golang/dep/cmd/dep
|
||||||
go get golang.org/x/tools/cmd/cover
|
go get -u golang.org/x/tools/cmd/cover
|
||||||
go get github.com/mitchellh/gox
|
go get -u github.com/mitchellh/gox
|
||||||
go get github.com/jteeuwen/go-bindata/...
|
go get -u github.com/go-bindata/go-bindata/...
|
||||||
godep restore
|
dep ensure
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@rm -f ./pgweb
|
@rm -f ./pgweb
|
||||||
@@ -84,4 +84,4 @@ docker-release:
|
|||||||
|
|
||||||
docker-push:
|
docker-push:
|
||||||
docker push $(DOCKER_RELEASE_TAG)
|
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.
|
Web-based PostgreSQL database browser written in Go.
|
||||||
|
|
||||||
[](https://github.com/sosedoff/pgweb/releases)
|
[](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://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
|
## 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
|
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
|
## Demo
|
||||||
|
|
||||||
Visit https://pgweb-demo.herokuapp.com to see pgweb in action.
|
Visit https://pgweb-demo.herokuapp.com to see pgweb in action.
|
||||||
@@ -118,6 +133,6 @@ make test-all
|
|||||||
|
|
||||||
## License
|
## 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:
|
environment:
|
||||||
GOPATH: c:\gopath
|
GOPATH: c:\gopath
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
|
||||||
services:
|
services:
|
||||||
- postgresql
|
- postgresql
|
||||||
@@ -13,15 +14,13 @@ install:
|
|||||||
- cd %APPVEYOR_BUILD_FOLDER%
|
- cd %APPVEYOR_BUILD_FOLDER%
|
||||||
- go env
|
- go env
|
||||||
- go version
|
- go version
|
||||||
- go get github.com/tools/godep
|
- go get github.com/golang/dep/cmd/dep
|
||||||
- godep get github.com/mitchellh/gox
|
- dep ensure
|
||||||
- godep get github.com/jteeuwen/go-bindata/...
|
|
||||||
- godep restore
|
|
||||||
|
|
||||||
build_script:
|
build_script:
|
||||||
- godep go build
|
- go build
|
||||||
|
|
||||||
test_script:
|
test_script:
|
||||||
- godep go test -cover ./...
|
- go test -cover ./...
|
||||||
|
|
||||||
deploy: off
|
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"
|
host = "localhost"
|
||||||
port = "5432"
|
port = 5432
|
||||||
user = "postgres"
|
user = "postgres"
|
||||||
database = "mydatabase"
|
database = "mydatabase"
|
||||||
ssl = "disable"
|
ssl = "disable"
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ host = "localhost"
|
|||||||
port = 5432
|
port = 5432
|
||||||
user = "postgres"
|
user = "postgres"
|
||||||
database = "mydatabase"
|
database = "mydatabase"
|
||||||
ssl = "disable"
|
ssl = "disabled"
|
||||||
@@ -17,6 +17,15 @@ CREATE DATABASE "booktown";
|
|||||||
-- Name: DATABASE "booktown" Type: COMMENT Owner:
|
-- 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.';
|
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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"github.com/sosedoff/pgweb/pkg/cli"
|
||||||
"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"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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() {
|
func main() {
|
||||||
initOptions()
|
cli.Run()
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|||||||
+214
-50
@@ -4,9 +4,12 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
neturl "net/url"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/tuvistavie/securerandom"
|
||||||
|
|
||||||
"github.com/sosedoff/pgweb/pkg/bookmarks"
|
"github.com/sosedoff/pgweb/pkg/bookmarks"
|
||||||
"github.com/sosedoff/pgweb/pkg/client"
|
"github.com/sosedoff/pgweb/pkg/client"
|
||||||
@@ -16,10 +19,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
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{}
|
DbSessions = map[string]*client.Client{}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DB returns a database connection from the client context
|
||||||
func DB(c *gin.Context) *client.Client {
|
func DB(c *gin.Context) *client.Client {
|
||||||
if command.Opts.Sessions {
|
if command.Opts.Sessions {
|
||||||
return DbSessions[getSessionId(c.Request)]
|
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
|
// In debug mode endpoint will return a lot of sensitive information
|
||||||
// like full database connection string and all query history.
|
// like full database connection string and all query history.
|
||||||
if command.Opts.Debug {
|
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
|
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) {
|
func Connect(c *gin.Context) {
|
||||||
|
if command.Opts.LockSession {
|
||||||
|
badRequest(c, "Session is locked")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var sshInfo *shared.SSHInfo
|
var sshInfo *shared.SSHInfo
|
||||||
url := c.Request.FormValue("url")
|
url := c.Request.FormValue("url")
|
||||||
|
|
||||||
if url == "" {
|
if url == "" {
|
||||||
c.JSON(400, Error{"Url parameter is required"})
|
badRequest(c, "Url parameter is required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := command.Options{Url: url}
|
opts := command.Options{Url: url}
|
||||||
url, err := connection.FormatUrl(opts)
|
url, err := connection.FormatURL(opts)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(400, Error{err.Error()})
|
badRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,67 +146,138 @@ func Connect(c *gin.Context) {
|
|||||||
|
|
||||||
cl, err := client.NewFromUrl(url, sshInfo)
|
cl, err := client.NewFromUrl(url, sshInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(400, Error{err.Error()})
|
badRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cl.Test()
|
err = cl.Test()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(400, Error{err.Error()})
|
badRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := cl.Info()
|
info, err := cl.Info()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = setClient(c, cl)
|
err = setClient(c, cl)
|
||||||
if err != nil {
|
}
|
||||||
cl.Close()
|
if err != nil {
|
||||||
c.JSON(400, Error{err.Error()})
|
cl.Close()
|
||||||
return
|
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) {
|
func Disconnect(c *gin.Context) {
|
||||||
|
if command.Opts.LockSession {
|
||||||
|
badRequest(c, "Session is locked")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
conn := DB(c)
|
conn := DB(c)
|
||||||
|
|
||||||
if conn == nil {
|
if conn == nil {
|
||||||
c.JSON(400, Error{"Not connected"})
|
badRequest(c, "Not connected")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := conn.Close()
|
err := conn.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(400, Error{err.Error()})
|
badRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(200, map[string]bool{"success": true})
|
successResponse(c, gin.H{"success": true})
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDatabases(c *gin.Context) {
|
func GetDatabases(c *gin.Context) {
|
||||||
|
conn := DB(c)
|
||||||
|
if conn.External {
|
||||||
|
errorResponse(c, 403, "Not permitted")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
names, err := DB(c).Databases()
|
names, err := DB(c).Databases()
|
||||||
serveResult(names, err, c)
|
serveResult(c, names, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetObjects(c *gin.Context) {
|
func GetObjects(c *gin.Context) {
|
||||||
result, err := DB(c).Objects()
|
result, err := DB(c).Objects()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(400, NewError(err))
|
badRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
successResponse(c, client.ObjectsFromResult(result))
|
||||||
objects := client.ObjectsFromResult(result)
|
|
||||||
c.JSON(200, objects)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunQuery(c *gin.Context) {
|
func RunQuery(c *gin.Context) {
|
||||||
query := cleanQuery(c.Request.FormValue("query"))
|
query := cleanQuery(c.Request.FormValue("query"))
|
||||||
|
|
||||||
if query == "" {
|
if query == "" {
|
||||||
c.JSON(400, NewError(errors.New("Query parameter is missing")))
|
badRequest(c, "Query parameter is missing")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +288,7 @@ func ExplainQuery(c *gin.Context) {
|
|||||||
query := cleanQuery(c.Request.FormValue("query"))
|
query := cleanQuery(c.Request.FormValue("query"))
|
||||||
|
|
||||||
if query == "" {
|
if query == "" {
|
||||||
c.JSON(400, NewError(errors.New("Query parameter is missing")))
|
badRequest(c, "Query parameter is missing")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +297,7 @@ func ExplainQuery(c *gin.Context) {
|
|||||||
|
|
||||||
func GetSchemas(c *gin.Context) {
|
func GetSchemas(c *gin.Context) {
|
||||||
res, err := DB(c).Schemas()
|
res, err := DB(c).Schemas()
|
||||||
serveResult(res, err, c)
|
serveResult(c, res, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetTable(c *gin.Context) {
|
func GetTable(c *gin.Context) {
|
||||||
@@ -183,19 +310,19 @@ func GetTable(c *gin.Context) {
|
|||||||
res, err = DB(c).Table(c.Params.ByName("table"))
|
res, err = DB(c).Table(c.Params.ByName("table"))
|
||||||
}
|
}
|
||||||
|
|
||||||
serveResult(res, err, c)
|
serveResult(c, res, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetTableRows(c *gin.Context) {
|
func GetTableRows(c *gin.Context) {
|
||||||
offset, err := parseIntFormValue(c, "offset", 0)
|
offset, err := parseIntFormValue(c, "offset", 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(400, NewError(err))
|
badRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
limit, err := parseIntFormValue(c, "limit", 100)
|
limit, err := parseIntFormValue(c, "limit", 100)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(400, NewError(err))
|
badRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,13 +336,13 @@ func GetTableRows(c *gin.Context) {
|
|||||||
|
|
||||||
res, err := DB(c).TableRows(c.Params.ByName("table"), opts)
|
res, err := DB(c).TableRows(c.Params.ByName("table"), opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(400, NewError(err))
|
badRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
countRes, err := DB(c).TableRowsCount(c.Params.ByName("table"), opts)
|
countRes, err := DB(c).TableRowsCount(c.Params.ByName("table"), opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(400, NewError(err))
|
badRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,48 +362,49 @@ func GetTableRows(c *gin.Context) {
|
|||||||
PerPage: numFetch,
|
PerPage: numFetch,
|
||||||
}
|
}
|
||||||
|
|
||||||
serveResult(res, err, c)
|
serveResult(c, res, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetTableInfo(c *gin.Context) {
|
func GetTableInfo(c *gin.Context) {
|
||||||
res, err := DB(c).TableInfo(c.Params.ByName("table"))
|
res, err := DB(c).TableInfo(c.Params.ByName("table"))
|
||||||
|
if err == nil {
|
||||||
if err != nil {
|
successResponse(c, res.Format()[0])
|
||||||
c.JSON(400, NewError(err))
|
} else {
|
||||||
return
|
badRequest(c, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(200, res.Format()[0])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetHistory(c *gin.Context) {
|
func GetHistory(c *gin.Context) {
|
||||||
c.JSON(200, DB(c).History)
|
successResponse(c, DB(c).History)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetConnectionInfo(c *gin.Context) {
|
func GetConnectionInfo(c *gin.Context) {
|
||||||
res, err := DB(c).Info()
|
res, err := DB(c).Info()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(400, NewError(err))
|
badRequest(c, err)
|
||||||
return
|
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) {
|
func GetActivity(c *gin.Context) {
|
||||||
res, err := DB(c).Activity()
|
res, err := DB(c).Activity()
|
||||||
serveResult(res, err, c)
|
serveResult(c, res, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetTableIndexes(c *gin.Context) {
|
func GetTableIndexes(c *gin.Context) {
|
||||||
res, err := DB(c).TableIndexes(c.Params.ByName("table"))
|
res, err := DB(c).TableIndexes(c.Params.ByName("table"))
|
||||||
serveResult(res, err, c)
|
serveResult(c, res, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetTableConstraints(c *gin.Context) {
|
func GetTableConstraints(c *gin.Context) {
|
||||||
res, err := DB(c).TableConstraints(c.Params.ByName("table"))
|
res, err := DB(c).TableConstraints(c.Params.ByName("table"))
|
||||||
serveResult(res, err, c)
|
serveResult(c, res, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleQuery(query string, c *gin.Context) {
|
func HandleQuery(query string, c *gin.Context) {
|
||||||
@@ -287,7 +415,7 @@ func HandleQuery(query string, c *gin.Context) {
|
|||||||
|
|
||||||
result, err := DB(c).Query(query)
|
result, err := DB(c).Query(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(400, NewError(err))
|
badRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,7 +434,7 @@ func HandleQuery(query string, c *gin.Context) {
|
|||||||
case "csv":
|
case "csv":
|
||||||
c.Data(200, "text/csv", result.CSV())
|
c.Data(200, "text/csv", result.CSV())
|
||||||
case "json":
|
case "json":
|
||||||
c.Data(200, "applicaiton/json", result.JSON())
|
c.Data(200, "application/json", result.JSON())
|
||||||
case "xml":
|
case "xml":
|
||||||
c.XML(200, result)
|
c.XML(200, result)
|
||||||
default:
|
default:
|
||||||
@@ -315,16 +443,52 @@ func HandleQuery(query string, c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetBookmarks(c *gin.Context) {
|
func GetBookmarks(c *gin.Context) {
|
||||||
bookmarks, err := bookmarks.ReadAll(bookmarks.Path())
|
bookmarks, err := bookmarks.ReadAll(bookmarks.Path(command.Opts.BookmarksDir))
|
||||||
serveResult(bookmarks, err, c)
|
serveResult(c, bookmarks, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetInfo(c *gin.Context) {
|
func GetInfo(c *gin.Context) {
|
||||||
info := map[string]string{
|
successResponse(c, gin.H{
|
||||||
"version": command.VERSION,
|
"version": command.Version,
|
||||||
"git_sha": command.GitCommit,
|
"git_sha": command.GitCommit,
|
||||||
"build_time": command.BuildTime,
|
"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.gif": "image/gif",
|
||||||
"foo.eot": "application/vnd.ms-fontobject",
|
"foo.eot": "application/vnd.ms-fontobject",
|
||||||
"foo.svg": "image/svg+xml",
|
"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.foo": "text/plain; charset=utf-8",
|
||||||
"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/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/sosedoff/pgweb/pkg/data"
|
||||||
"github.com/sosedoff/pgweb/pkg/shared"
|
"github.com/sosedoff/pgweb/pkg/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
var extraMimeTypes = map[string]string{
|
var (
|
||||||
".icon": "image-x-icon",
|
// Mime types definitions
|
||||||
".ttf": "application/x-font-ttf",
|
extraMimeTypes = map[string]string{
|
||||||
".woff": "application/x-font-woff",
|
".icon": "image-x-icon",
|
||||||
".eot": "application/vnd.ms-fontobject",
|
".ttf": "application/x-font-ttf",
|
||||||
".svg": "image/svg+xml",
|
".woff": "application/x-font-woff",
|
||||||
".html": "text/html; charset-utf-8",
|
".eot": "application/vnd.ms-fontobject",
|
||||||
}
|
".svg": "image/svg+xml",
|
||||||
|
".html": "text/html; charset-utf-8",
|
||||||
|
}
|
||||||
|
|
||||||
// Paths that dont require database connection
|
// Paths that dont require database connection
|
||||||
var allowedPaths = map[string]bool{
|
allowedPaths = map[string]bool{
|
||||||
"/api/sessions": true,
|
"/api/sessions": true,
|
||||||
"/api/info": true,
|
"/api/info": true,
|
||||||
"/api/connect": true,
|
"/api/connect": true,
|
||||||
"/api/bookmarks": true,
|
"/api/bookmarks": true,
|
||||||
"/api/history": true,
|
"/api/history": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// List of characters replaced by javascript code to make queries url-safe.
|
// List of characters replaced by javascript code to make queries url-safe.
|
||||||
var base64subs = map[string]string{
|
base64subs = map[string]string{
|
||||||
"-": "+",
|
"-": "+",
|
||||||
"_": "/",
|
"_": "/",
|
||||||
".": "=",
|
".": "=",
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
type Error struct {
|
type Error struct {
|
||||||
Message string `json:"error"`
|
Message string `json:"error"`
|
||||||
@@ -115,6 +119,7 @@ func parseSshInfo(c *gin.Context) *shared.SSHInfo {
|
|||||||
Port: c.Request.FormValue("ssh_port"),
|
Port: c.Request.FormValue("ssh_port"),
|
||||||
User: c.Request.FormValue("ssh_user"),
|
User: c.Request.FormValue("ssh_user"),
|
||||||
Password: c.Request.FormValue("ssh_password"),
|
Password: c.Request.FormValue("ssh_password"),
|
||||||
|
Key: c.Request.FormValue("ssh_key"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if info.Port == "" {
|
if info.Port == "" {
|
||||||
@@ -138,3 +143,48 @@ func assetContentType(name string) string {
|
|||||||
|
|
||||||
return result
|
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
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,3 +39,34 @@ func Test_getSessionId(t *testing.T) {
|
|||||||
req.URL, _ = url.Parse("http://foobar/?_session_id=token")
|
req.URL, _ = url.Parse("http://foobar/?_session_id=token")
|
||||||
assert.Equal(t, "token", getSessionId(req))
|
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/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/sosedoff/pgweb/pkg/command"
|
"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 {
|
func dbCheckMiddleware() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
path := strings.Replace(c.Request.URL.Path, command.Opts.Prefix, "", -1)
|
path := strings.Replace(c.Request.URL.Path, command.Opts.Prefix, "", -1)
|
||||||
|
|
||||||
if allowedPaths[path] == true {
|
// Allow whitelisted paths
|
||||||
|
if allowedPaths[path] {
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// We dont care about sessions unless they're enabled
|
// Check if session exists in single-session mode
|
||||||
if !command.Opts.Sessions {
|
if !command.Opts.Sessions {
|
||||||
if DbClient == nil {
|
if DbClient == nil {
|
||||||
c.JSON(400, Error{"Not connected"})
|
badRequest(c, "Not connected")
|
||||||
c.Abort()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,17 +31,17 @@ func dbCheckMiddleware() gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine session ID from the client request
|
||||||
sessionId := getSessionId(c.Request)
|
sessionId := getSessionId(c.Request)
|
||||||
if sessionId == "" {
|
if sessionId == "" {
|
||||||
c.JSON(400, Error{"Session ID is required"})
|
badRequest(c, "Session ID is required")
|
||||||
c.Abort()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine the database connection handle for the session
|
||||||
conn := DbSessions[sessionId]
|
conn := DbSessions[sessionId]
|
||||||
if conn == nil {
|
if conn == nil {
|
||||||
c.JSON(400, Error{"Not connected"})
|
badRequest(c, "Not connected")
|
||||||
c.Abort()
|
|
||||||
return
|
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 {
|
func requestInspectMiddleware() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
err := c.Request.ParseForm()
|
err := c.Request.ParseForm()
|
||||||
@@ -58,21 +57,11 @@ func requestInspectMiddleware() gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveStaticAsset(path string, c *gin.Context) {
|
// Middleware to inject CORS headers
|
||||||
data, err := data.Asset("static" + path)
|
func corsMiddleware() gin.HandlerFunc {
|
||||||
if err != nil {
|
return func(c *gin.Context) {
|
||||||
c.String(400, err.Error())
|
c.Header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||||
return
|
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())
|
group.Use(requestInspectMiddleware())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if command.Opts.Cors {
|
||||||
|
group.Use(corsMiddleware())
|
||||||
|
}
|
||||||
|
|
||||||
group.Use(dbCheckMiddleware())
|
group.Use(dbCheckMiddleware())
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetupRoutes(router *gin.Engine) {
|
func SetupRoutes(router *gin.Engine) {
|
||||||
group := router.Group(command.Opts.Prefix)
|
root := router.Group(command.Opts.Prefix)
|
||||||
|
|
||||||
group.GET("/", GetHome)
|
root.GET("/", GetHome)
|
||||||
group.GET("/static/*path", GetAsset)
|
root.GET("/static/*path", GetAsset)
|
||||||
|
root.GET("/connect/:resource", ConnectWithBackend)
|
||||||
|
|
||||||
api := group.Group("/api")
|
api := root.Group("/api")
|
||||||
{
|
SetupMiddlewares(api)
|
||||||
SetupMiddlewares(api)
|
|
||||||
|
|
||||||
if command.Opts.Sessions {
|
if command.Opts.Sessions {
|
||||||
api.GET("/sessions", GetSessions)
|
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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/BurntSushi/toml"
|
||||||
"github.com/mitchellh/go-homedir"
|
"github.com/mitchellh/go-homedir"
|
||||||
|
|
||||||
|
"github.com/sosedoff/pgweb/pkg/command"
|
||||||
"github.com/sosedoff/pgweb/pkg/shared"
|
"github.com/sosedoff/pgweb/pkg/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Bookmark struct {
|
type Bookmark struct {
|
||||||
Url string `json:"url"` // Postgres connection URL
|
Url string `json:"url"` // Postgres connection URL
|
||||||
Host string `json:"host"` // Server hostname
|
Host string `json:"host"` // Server hostname
|
||||||
Port string `json:"port"` // Server port
|
Port int `json:"port"` // Server port
|
||||||
User string `json:"user"` // Database user
|
User string `json:"user"` // Database user
|
||||||
Password string `json:"password"` // User password
|
Password string `json:"password"` // User password
|
||||||
Database string `json:"database"` // Database name
|
Database string `json:"database"` // Database name
|
||||||
Ssl string `json:"ssl"` // Connection SSL mode
|
Ssl string `json:"ssl"` // Connection SSL mode
|
||||||
Ssh shared.SSHInfo `json:"ssh"` // SSH tunnel config
|
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) {
|
func readServerConfig(path string) (Bookmark, error) {
|
||||||
@@ -32,6 +49,32 @@ func readServerConfig(path string) (Bookmark, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_, err = toml.Decode(string(buff), &bookmark)
|
_, 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
|
return bookmark, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,9 +83,13 @@ func fileBasename(path string) string {
|
|||||||
return strings.Replace(filename, filepath.Ext(path), "", 1)
|
return strings.Replace(filename, filepath.Ext(path), "", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Path() string {
|
func Path(overrideDir string) string {
|
||||||
path, _ := homedir.Dir()
|
if overrideDir == "" {
|
||||||
return fmt.Sprintf("%s/.pgweb/bookmarks", path)
|
path, _ := homedir.Dir()
|
||||||
|
return fmt.Sprintf("%s/.pgweb/bookmarks", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return overrideDir
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReadAll(path string) (map[string]Bookmark, error) {
|
func ReadAll(path string) (map[string]Bookmark, error) {
|
||||||
@@ -72,3 +119,17 @@ func ReadAll(path string) (map[string]Bookmark, error) {
|
|||||||
|
|
||||||
return results, nil
|
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 (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sosedoff/pgweb/pkg/command"
|
||||||
|
"github.com/sosedoff/pgweb/pkg/shared"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,24 +14,23 @@ func Test_Invalid_Bookmark_Files(t *testing.T) {
|
|||||||
|
|
||||||
_, err = readServerConfig("../../data/invalid.toml")
|
_, err = readServerConfig("../../data/invalid.toml")
|
||||||
assert.Error(t, err)
|
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())
|
assert.Equal(t, "Near line 1 (last key parsed 'invalid'): expected key separator '=', but got 'e' 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())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_Bookmark(t *testing.T) {
|
func Test_Bookmark(t *testing.T) {
|
||||||
bookmark, err := readServerConfig("../../data/bookmark.toml")
|
bookmark, err := readServerConfig("../../data/bookmark.toml")
|
||||||
|
|
||||||
assert.Equal(t, nil, err)
|
assert.Equal(t, nil, err)
|
||||||
assert.Equal(t, "localhost", bookmark.Host)
|
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, "postgres", bookmark.User)
|
||||||
assert.Equal(t, "mydatabase", bookmark.Database)
|
assert.Equal(t, "mydatabase", bookmark.Database)
|
||||||
assert.Equal(t, "disable", bookmark.Ssl)
|
assert.Equal(t, "disable", bookmark.Ssl)
|
||||||
assert.Equal(t, "", bookmark.Password)
|
assert.Equal(t, "", bookmark.Password)
|
||||||
assert.Equal(t, "", bookmark.Url)
|
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) {
|
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, nil, err)
|
||||||
assert.Equal(t, "postgres://username:password@host:port/database?sslmode=disable", bookmark.Url)
|
assert.Equal(t, "postgres://username:password@host:port/database?sslmode=disable", bookmark.Url)
|
||||||
assert.Equal(t, "", bookmark.Host)
|
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.User)
|
||||||
assert.Equal(t, "", bookmark.Database)
|
assert.Equal(t, "", bookmark.Database)
|
||||||
assert.Equal(t, "", bookmark.Ssl)
|
assert.Equal(t, "disable", bookmark.Ssl)
|
||||||
assert.Equal(t, "", bookmark.Password)
|
assert.Equal(t, "", bookmark.Password)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_Bookmarks_Path(t *testing.T) {
|
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) {
|
func Test_Basename(t *testing.T) {
|
||||||
@@ -67,5 +68,74 @@ func Test_ReadBookmarks(t *testing.T) {
|
|||||||
bookmarks, err := ReadAll("../../data")
|
bookmarks, err := ReadAll("../../data")
|
||||||
|
|
||||||
assert.Equal(t, nil, err)
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
neturl "net/url"
|
neturl "net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
_ "github.com/lib/pq"
|
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
|
||||||
"github.com/sosedoff/pgweb/pkg/command"
|
"github.com/sosedoff/pgweb/pkg/command"
|
||||||
"github.com/sosedoff/pgweb/pkg/connection"
|
"github.com/sosedoff/pgweb/pkg/connection"
|
||||||
"github.com/sosedoff/pgweb/pkg/history"
|
"github.com/sosedoff/pgweb/pkg/history"
|
||||||
@@ -16,9 +19,21 @@ import (
|
|||||||
"github.com/sosedoff/pgweb/pkg/statements"
|
"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 {
|
type Client struct {
|
||||||
db *sqlx.DB
|
db *sqlx.DB
|
||||||
tunnel *Tunnel
|
tunnel *Tunnel
|
||||||
|
serverVersion string
|
||||||
|
serverType string
|
||||||
|
lastQueryTime time.Time
|
||||||
|
External bool
|
||||||
History []history.Record `json:"history"`
|
History []history.Record `json:"history"`
|
||||||
ConnectionString string `json:"connection_string"`
|
ConnectionString string `json:"connection_string"`
|
||||||
}
|
}
|
||||||
@@ -41,7 +56,7 @@ func getSchemaAndTable(str string) (string, string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New() (*Client, error) {
|
func New() (*Client, error) {
|
||||||
str, err := connection.BuildString(command.Opts)
|
str, err := connection.BuildStringFromOptions(command.Opts)
|
||||||
|
|
||||||
if command.Opts.Debug && str != "" {
|
if command.Opts.Debug && str != "" {
|
||||||
fmt.Println("Creating a new client for:", str)
|
fmt.Println("Creating a new client for:", str)
|
||||||
@@ -62,6 +77,7 @@ func New() (*Client, error) {
|
|||||||
History: history.New(),
|
History: history.New(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
client.setServerVersion()
|
||||||
return &client, nil
|
return &client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +85,9 @@ func NewFromUrl(url string, sshInfo *shared.SSHInfo) (*Client, error) {
|
|||||||
var tunnel *Tunnel
|
var tunnel *Tunnel
|
||||||
|
|
||||||
if sshInfo != nil {
|
if sshInfo != nil {
|
||||||
|
if command.Opts.DisableSSH {
|
||||||
|
return nil, fmt.Errorf("ssh connections are disabled")
|
||||||
|
}
|
||||||
if command.Opts.Debug {
|
if command.Opts.Debug {
|
||||||
fmt.Println("Opening SSH tunnel for:", sshInfo)
|
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)
|
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)
|
db, err := sqlx.Open("postgres", url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -113,36 +137,62 @@ func NewFromUrl(url string, sshInfo *shared.SSHInfo) (*Client, error) {
|
|||||||
History: history.New(),
|
History: history.New(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
client.setServerVersion()
|
||||||
return &client, nil
|
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 {
|
func (client *Client) Test() error {
|
||||||
return client.db.Ping()
|
return client.db.Ping()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) Info() (*Result, error) {
|
func (client *Client) Info() (*Result, error) {
|
||||||
return client.query(statements.PG_INFO)
|
return client.query(statements.Info)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) Databases() ([]string, error) {
|
func (client *Client) Databases() ([]string, error) {
|
||||||
return client.fetchRows(statements.PG_DATABASES)
|
return client.fetchRows(statements.Databases)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) Schemas() ([]string, error) {
|
func (client *Client) Schemas() ([]string, error) {
|
||||||
return client.fetchRows(statements.PG_SCHEMAS)
|
return client.fetchRows(statements.Schemas)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) Objects() (*Result, error) {
|
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) {
|
func (client *Client) Table(table string) (*Result, error) {
|
||||||
schema, table := getSchemaAndTable(table)
|
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) {
|
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) {
|
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"
|
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 {
|
if opts.Limit > 0 {
|
||||||
@@ -172,9 +222,34 @@ func (client *Client) TableRows(table string, opts RowsOptions) (*Result, error)
|
|||||||
return client.query(sql)
|
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)
|
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 != "" {
|
if opts.Where != "" {
|
||||||
sql += fmt.Sprintf(" WHERE %s", 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) {
|
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) {
|
func (client *Client) TableIndexes(table string) (*Result, error) {
|
||||||
schema, table := getSchemaAndTable(table)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -200,7 +279,7 @@ func (client *Client) TableIndexes(table string) (*Result, error) {
|
|||||||
|
|
||||||
func (client *Client) TableConstraints(table string) (*Result, error) {
|
func (client *Client) TableConstraints(table string) (*Result, error) {
|
||||||
schema, table := getSchemaAndTable(table)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -211,7 +290,17 @@ func (client *Client) TableConstraints(table string) (*Result, error) {
|
|||||||
|
|
||||||
// Returns all active queriers on the server
|
// Returns all active queriers on the server
|
||||||
func (client *Client) Activity() (*Result, error) {
|
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) {
|
func (client *Client) Query(query string) (*Result, error) {
|
||||||
@@ -225,7 +314,38 @@ func (client *Client) Query(query string) (*Result, error) {
|
|||||||
return res, err
|
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) {
|
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])
|
action := strings.ToLower(strings.Split(query, " ")[0])
|
||||||
if action == "update" || action == "delete" {
|
if action == "update" || action == "delete" {
|
||||||
res, err := client.db.Exec(query, args...)
|
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...)
|
rows, err := client.db.Queryx(query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if command.Opts.Debug {
|
||||||
|
log.Println("Failed query:", query, "\nArgs:", args)
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
cols, err := rows.Columns()
|
cols, err := rows.Columns()
|
||||||
@@ -308,6 +430,16 @@ func (client *Client) Close() error {
|
|||||||
return nil
|
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
|
// Fetch all rows as strings for a single column
|
||||||
func (client *Client) fetchRows(q string) ([]string, error) {
|
func (client *Client) fetchRows(q string) ([]string, error) {
|
||||||
res, err := client.query(q)
|
res, err := client.query(q)
|
||||||
|
|||||||
+168
-38
@@ -2,10 +2,14 @@ package client
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sosedoff/pgweb/pkg/command"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@@ -43,6 +47,11 @@ func getVar(name, def string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func initVars() {
|
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")
|
serverHost = getVar("PGHOST", "localhost")
|
||||||
serverPort = getVar("PGPORT", "5432")
|
serverPort = getVar("PGPORT", "5432")
|
||||||
serverUser = getVar("PGUSER", "postgres")
|
serverUser = getVar("PGUSER", "postgres")
|
||||||
@@ -69,6 +78,9 @@ func onWindows() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setup() {
|
func setup() {
|
||||||
|
// No pretty JSON for testsm
|
||||||
|
command.Opts.DisablePrettyJson = true
|
||||||
|
|
||||||
out, err := exec.Command(
|
out, err := exec.Command(
|
||||||
testCommands["createdb"],
|
testCommands["createdb"],
|
||||||
"-U", serverUser,
|
"-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)
|
url := fmt.Sprintf("postgres://%s@%s:%s/%s?sslmode=disable", serverUser, serverHost, serverPort, serverDatabase)
|
||||||
client, err := NewFromUrl(url, nil)
|
client, err := NewFromUrl(url, nil)
|
||||||
|
|
||||||
@@ -136,7 +148,7 @@ func test_NewClientFromUrl(t *testing.T) {
|
|||||||
assert.Equal(t, url, client.ConnectionString)
|
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)
|
url := fmt.Sprintf("postgresql://%s@%s:%s/%s?sslmode=disable", serverUser, serverHost, serverPort, serverDatabase)
|
||||||
client, err := NewFromUrl(url, nil)
|
client, err := NewFromUrl(url, nil)
|
||||||
|
|
||||||
@@ -148,18 +160,40 @@ func test_NewClientFromUrl2(t *testing.T) {
|
|||||||
assert.Equal(t, url, client.ConnectionString)
|
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())
|
assert.Equal(t, nil, testClient.Test())
|
||||||
}
|
}
|
||||||
|
|
||||||
func test_Info(t *testing.T) {
|
func testInfo(t *testing.T) {
|
||||||
res, err := testClient.Info()
|
res, err := testClient.Info()
|
||||||
|
|
||||||
assert.Equal(t, nil, err)
|
assert.Equal(t, nil, err)
|
||||||
assert.NotEqual(t, nil, res)
|
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()
|
res, err := testClient.Databases()
|
||||||
|
|
||||||
assert.Equal(t, nil, err)
|
assert.Equal(t, nil, err)
|
||||||
@@ -167,7 +201,7 @@ func test_Databases(t *testing.T) {
|
|||||||
assert.Contains(t, res, "postgres")
|
assert.Contains(t, res, "postgres")
|
||||||
}
|
}
|
||||||
|
|
||||||
func test_Objects(t *testing.T) {
|
func testObjects(t *testing.T) {
|
||||||
res, err := testClient.Objects()
|
res, err := testClient.Objects()
|
||||||
objects := ObjectsFromResult(res)
|
objects := ObjectsFromResult(res)
|
||||||
|
|
||||||
@@ -180,6 +214,7 @@ func test_Objects(t *testing.T) {
|
|||||||
"customers",
|
"customers",
|
||||||
"daily_inventory",
|
"daily_inventory",
|
||||||
"distinguished_authors",
|
"distinguished_authors",
|
||||||
|
"dummies",
|
||||||
"editions",
|
"editions",
|
||||||
"employees",
|
"employees",
|
||||||
"favorite_authors",
|
"favorite_authors",
|
||||||
@@ -198,7 +233,7 @@ func test_Objects(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, nil, err)
|
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, []string{"public"}, mapKeys(objects))
|
||||||
assert.Equal(t, tables, objects["public"].Tables)
|
assert.Equal(t, tables, objects["public"].Tables)
|
||||||
assert.Equal(t, []string{"recent_shipments", "stock_view"}, objects["public"].Views)
|
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")
|
res, err := testClient.Table("books")
|
||||||
|
|
||||||
columns := []string{
|
columns := []string{
|
||||||
@@ -222,6 +257,7 @@ func test_Table(t *testing.T) {
|
|||||||
"character_maximum_length",
|
"character_maximum_length",
|
||||||
"character_set_catalog",
|
"character_set_catalog",
|
||||||
"column_default",
|
"column_default",
|
||||||
|
"comment",
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, nil, err)
|
assert.Equal(t, nil, err)
|
||||||
@@ -229,7 +265,7 @@ func test_Table(t *testing.T) {
|
|||||||
assert.Equal(t, 4, len(res.Rows))
|
assert.Equal(t, 4, len(res.Rows))
|
||||||
}
|
}
|
||||||
|
|
||||||
func test_TableRows(t *testing.T) {
|
func testTableRows(t *testing.T) {
|
||||||
res, err := testClient.TableRows("books", RowsOptions{})
|
res, err := testClient.TableRows("books", RowsOptions{})
|
||||||
|
|
||||||
assert.Equal(t, nil, err)
|
assert.Equal(t, nil, err)
|
||||||
@@ -237,7 +273,7 @@ func test_TableRows(t *testing.T) {
|
|||||||
assert.Equal(t, 15, len(res.Rows))
|
assert.Equal(t, 15, len(res.Rows))
|
||||||
}
|
}
|
||||||
|
|
||||||
func test_TableInfo(t *testing.T) {
|
func testTableInfo(t *testing.T) {
|
||||||
res, err := testClient.TableInfo("books")
|
res, err := testClient.TableInfo("books")
|
||||||
|
|
||||||
assert.Equal(t, nil, err)
|
assert.Equal(t, nil, err)
|
||||||
@@ -245,7 +281,36 @@ func test_TableInfo(t *testing.T) {
|
|||||||
assert.Equal(t, 1, len(res.Rows))
|
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")
|
res, err := testClient.TableIndexes("books")
|
||||||
|
|
||||||
assert.Equal(t, nil, err)
|
assert.Equal(t, nil, err)
|
||||||
@@ -253,15 +318,39 @@ func test_TableIndexes(t *testing.T) {
|
|||||||
assert.Equal(t, 2, len(res.Rows))
|
assert.Equal(t, 2, len(res.Rows))
|
||||||
}
|
}
|
||||||
|
|
||||||
func test_TableConstraints(t *testing.T) {
|
func testTableConstraints(t *testing.T) {
|
||||||
res, err := testClient.TableConstraints("editions")
|
res, err := testClient.TableConstraints("editions")
|
||||||
|
|
||||||
assert.Equal(t, nil, err)
|
assert.Equal(t, nil, err)
|
||||||
assert.Equal(t, 1, len(res.Columns))
|
assert.Equal(t, []string{"name", "definition"}, res.Columns)
|
||||||
assert.Equal(t, 2, len(res.Rows))
|
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")
|
res, err := testClient.Query("SELECT * FROM books")
|
||||||
|
|
||||||
assert.Equal(t, nil, err)
|
assert.Equal(t, nil, err)
|
||||||
@@ -269,7 +358,7 @@ func test_Query(t *testing.T) {
|
|||||||
assert.Equal(t, 15, len(res.Rows))
|
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")
|
res, err := testClient.Query("SELCT * FROM books")
|
||||||
|
|
||||||
assert.NotEqual(t, nil, err)
|
assert.NotEqual(t, nil, err)
|
||||||
@@ -277,7 +366,7 @@ func test_QueryError(t *testing.T) {
|
|||||||
assert.Equal(t, true, res == nil)
|
assert.Equal(t, true, res == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func test_QueryInvalidTable(t *testing.T) {
|
func testQueryInvalidTable(t *testing.T) {
|
||||||
res, err := testClient.Query("SELECT * FROM books2")
|
res, err := testClient.Query("SELECT * FROM books2")
|
||||||
|
|
||||||
assert.NotEqual(t, nil, err)
|
assert.NotEqual(t, nil, err)
|
||||||
@@ -285,7 +374,25 @@ func test_QueryInvalidTable(t *testing.T) {
|
|||||||
assert.Equal(t, true, res == nil)
|
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")
|
res, _ := testClient.Query("SELECT * FROM books ORDER BY id ASC LIMIT 1")
|
||||||
csv := res.CSV()
|
csv := res.CSV()
|
||||||
|
|
||||||
@@ -294,7 +401,7 @@ func test_ResultCsv(t *testing.T) {
|
|||||||
assert.Equal(t, expected, string(csv))
|
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")
|
_, err := testClient.Query("SELECT * FROM books WHERE id = 12345")
|
||||||
query := testClient.History[len(testClient.History)-1].Query
|
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)
|
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")
|
_, err := testClient.Query("SELECT * FROM books123")
|
||||||
query := testClient.History[len(testClient.History)-1].Query
|
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)
|
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)
|
url := fmt.Sprintf("postgres://%s@%s:%s/%s?sslmode=disable", serverUser, serverHost, serverPort, serverDatabase)
|
||||||
client, _ := NewFromUrl(url, nil)
|
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)
|
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) {
|
func TestAll(t *testing.T) {
|
||||||
if onWindows() {
|
if onWindows() {
|
||||||
// Dont have access to windows machines at the moment...
|
t.Log("Unit testing on Windows platform is not supported.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,22 +452,33 @@ func TestAll(t *testing.T) {
|
|||||||
setup()
|
setup()
|
||||||
setupClient()
|
setupClient()
|
||||||
|
|
||||||
test_NewClientFromUrl(t)
|
testNewClientFromUrl(t)
|
||||||
test_Test(t)
|
testClientIdleTime(t)
|
||||||
test_Info(t)
|
testTest(t)
|
||||||
test_Databases(t)
|
testInfo(t)
|
||||||
test_Objects(t)
|
testActivity(t)
|
||||||
test_Table(t)
|
testDatabases(t)
|
||||||
test_TableRows(t)
|
testObjects(t)
|
||||||
test_TableInfo(t)
|
testTable(t)
|
||||||
test_TableIndexes(t)
|
testTableRows(t)
|
||||||
test_TableConstraints(t)
|
testTableInfo(t)
|
||||||
test_Query(t)
|
testEstimatedTableRowsCount(t)
|
||||||
test_QueryError(t)
|
testTableRowsCount(t)
|
||||||
test_QueryInvalidTable(t)
|
testTableRowsCountWithLargeTable(t)
|
||||||
test_ResultCsv(t)
|
testTableIndexes(t)
|
||||||
test_History(t)
|
testTableConstraints(t)
|
||||||
test_HistoryError(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()
|
teardownClient()
|
||||||
teardown()
|
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/csv"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/sosedoff/pgweb/pkg/command"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Row []interface{}
|
type Row []interface{}
|
||||||
@@ -49,6 +52,14 @@ func (res *Result) PrepareBigints() {
|
|||||||
}
|
}
|
||||||
case reflect.Float64:
|
case reflect.Float64:
|
||||||
val := col.(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 {
|
if val < -999999999999999 || val > 999999999999999 {
|
||||||
res.Rows[i][j] = strconv.FormatFloat(val, 'e', -1, 64)
|
res.Rows[i][j] = strconv.FormatFloat(val, 'e', -1, 64)
|
||||||
}
|
}
|
||||||
@@ -108,7 +119,14 @@ func (res *Result) CSV() []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (res *Result) JSON() []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
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+35
-5
@@ -10,6 +10,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
|
|
||||||
@@ -18,10 +19,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PORT_START = 29168
|
portStart = 29168
|
||||||
PORT_LIMIT = 500
|
portLimit = 500
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Tunnel represents the connection between local and remote server
|
||||||
type Tunnel struct {
|
type Tunnel struct {
|
||||||
TargetHost string
|
TargetHost string
|
||||||
TargetPort string
|
TargetPort string
|
||||||
@@ -36,6 +38,14 @@ func privateKeyPath() string {
|
|||||||
return os.Getenv("HOME") + "/.ssh/id_rsa"
|
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 {
|
func fileExists(path string) bool {
|
||||||
_, err := os.Stat(path)
|
_, err := os.Stat(path)
|
||||||
return err == nil
|
return err == nil
|
||||||
@@ -53,7 +63,14 @@ func parsePrivateKey(keyPath string) (ssh.Signer, error) {
|
|||||||
func makeConfig(info *shared.SSHInfo) (*ssh.ClientConfig, error) {
|
func makeConfig(info *shared.SSHInfo) (*ssh.ClientConfig, error) {
|
||||||
methods := []ssh.AuthMethod{}
|
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) {
|
if fileExists(keyPath) {
|
||||||
key, err := parsePrivateKey(keyPath)
|
key, err := parsePrivateKey(keyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -65,7 +82,16 @@ func makeConfig(info *shared.SSHInfo) (*ssh.ClientConfig, error) {
|
|||||||
|
|
||||||
methods = append(methods, ssh.Password(info.Password))
|
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 {
|
func (tunnel *Tunnel) sshEndpoint() string {
|
||||||
@@ -99,6 +125,7 @@ func (tunnel *Tunnel) handleConnection(local net.Conn) {
|
|||||||
local.Close()
|
local.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close closes the tunnel connection
|
||||||
func (tunnel *Tunnel) Close() {
|
func (tunnel *Tunnel) Close() {
|
||||||
if tunnel.Client != nil {
|
if tunnel.Client != nil {
|
||||||
tunnel.Client.Close()
|
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 {
|
func (tunnel *Tunnel) Configure() error {
|
||||||
config, err := makeConfig(tunnel.SSHInfo)
|
config, err := makeConfig(tunnel.SSHInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -131,6 +159,7 @@ func (tunnel *Tunnel) Configure() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start starts the connection handler loop
|
||||||
func (tunnel *Tunnel) Start() {
|
func (tunnel *Tunnel) Start() {
|
||||||
defer tunnel.Close()
|
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) {
|
func NewTunnel(sshInfo *shared.SSHInfo, dbUrl string) (*Tunnel, error) {
|
||||||
uri, err := url.Parse(dbUrl)
|
uri, err := url.Parse(dbUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
listenPort, err := connection.AvailablePort(PORT_START, PORT_LIMIT)
|
listenPort, err := connection.FindAvailablePort(portStart, portLimit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
|
"os/user"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/jessevdk/go-flags"
|
"github.com/jessevdk/go-flags"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Version bool `short:"v" long:"version" description:"Print version"`
|
Version bool `short:"v" long:"version" description:"Print version"`
|
||||||
Debug bool `short:"d" long:"debug" description:"Enable debugging mode" default:"false"`
|
Debug bool `short:"d" long:"debug" description:"Enable debugging mode"`
|
||||||
Url string `long:"url" description:"Database connection string"`
|
Url string `long:"url" description:"Database connection string"`
|
||||||
Host string `long:"host" description:"Server hostname or IP"`
|
Host string `long:"host" description:"Server hostname or IP" default:"localhost"`
|
||||||
Port int `long:"port" description:"Server port" default:"5432"`
|
Port int `long:"port" description:"Server port" default:"5432"`
|
||||||
User string `long:"user" description:"Database user"`
|
User string `long:"user" description:"Database user"`
|
||||||
Pass string `long:"pass" description:"Password for user"`
|
Pass string `long:"pass" description:"Password for user"`
|
||||||
DbName string `long:"db" description:"Database name"`
|
DbName string `long:"db" description:"Database name"`
|
||||||
Ssl string `long:"ssl" description:"SSL option"`
|
Ssl string `long:"ssl" description:"SSL option"`
|
||||||
HttpHost string `long:"bind" description:"HTTP server host" default:"localhost"`
|
HttpHost string `long:"bind" description:"HTTP server host" default:"localhost"`
|
||||||
HttpPort uint `long:"listen" description:"HTTP server listen port" default:"8081"`
|
HttpPort uint `long:"listen" description:"HTTP server listen port" default:"8081"`
|
||||||
AuthUser string `long:"auth-user" description:"HTTP basic auth user"`
|
AuthUser string `long:"auth-user" description:"HTTP basic auth user"`
|
||||||
AuthPass string `long:"auth-pass" description:"HTTP basic auth password"`
|
AuthPass string `long:"auth-pass" description:"HTTP basic auth password"`
|
||||||
SkipOpen bool `short:"s" long:"skip-open" description:"Skip browser open on start"`
|
SkipOpen bool `short:"s" long:"skip-open" description:"Skip browser open on start"`
|
||||||
Sessions bool `long:"sessions" description:"Enable multiple database sessions" default:"false"`
|
Sessions bool `long:"sessions" description:"Enable multiple database sessions"`
|
||||||
Prefix string `long:"prefix" description:"Add a url prefix"`
|
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
|
var Opts Options
|
||||||
|
|
||||||
func ParseOptions() error {
|
func ParseOptions(args []string) (Options, error) {
|
||||||
_, err := flags.ParseArgs(&Opts, os.Args)
|
var opts = Options{}
|
||||||
|
|
||||||
|
_, err := flags.ParseArgs(&opts, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return opts, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if Opts.Url == "" {
|
if opts.Url == "" {
|
||||||
Opts.Url = os.Getenv("DATABASE_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") != "" {
|
if os.Getenv("SESSIONS") != "" {
|
||||||
Opts.Sessions = true
|
opts.Sessions = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if Opts.Prefix != "" && !strings.Contains(Opts.Prefix, "/") {
|
if os.Getenv("LOCK_SESSION") != "" {
|
||||||
Opts.Prefix = Opts.Prefix + "/"
|
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
|
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
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_Options(t *testing.T) {
|
func TestParseOptions(t *testing.T) {
|
||||||
err := ParseOptions()
|
// Test default behavior
|
||||||
|
opts, err := ParseOptions([]string{})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, false, Opts.Sessions)
|
assert.Equal(t, false, opts.Sessions)
|
||||||
assert.Equal(t, "", Opts.Prefix)
|
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) {
|
// Test sessions
|
||||||
oldargs := os.Args
|
opts, err = ParseOptions([]string{"--sessions", "1"})
|
||||||
defer func() { os.Args = oldargs }()
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, true, opts.Sessions)
|
||||||
|
|
||||||
os.Args = []string{"--sessions", "1"}
|
// Test url prefix
|
||||||
assert.NoError(t, ParseOptions())
|
opts, err = ParseOptions([]string{"--prefix", "pgweb"})
|
||||||
assert.Equal(t, true, Opts.Sessions)
|
assert.NoError(t, err)
|
||||||
}
|
assert.Equal(t, "pgweb/", opts.Prefix)
|
||||||
|
|
||||||
func Test_PrefixOption(t *testing.T) {
|
opts, err = ParseOptions([]string{"--prefix", "pgweb/"})
|
||||||
oldargs := os.Args
|
assert.NoError(t, err)
|
||||||
defer func() { os.Args = oldargs }()
|
assert.Equal(t, "pgweb/", opts.Prefix)
|
||||||
|
|
||||||
os.Args = []string{"--prefix", "pgweb"}
|
// Test connect backend options
|
||||||
assert.NoError(t, ParseOptions())
|
opts, err = ParseOptions([]string{"--connect-backend", "test"})
|
||||||
assert.Equal(t, "pgweb/", Opts.Prefix)
|
assert.EqualError(t, err, "--sessions flag must be set")
|
||||||
|
|
||||||
os.Args = []string{"--prefix", "pgweb/"}
|
opts, err = ParseOptions([]string{"--connect-backend", "test", "--sessions"})
|
||||||
assert.NoError(t, ParseOptions())
|
assert.EqualError(t, err, "--connect-token flag must be set")
|
||||||
assert.Equal(t, "pgweb/", Opts.Prefix)
|
|
||||||
|
opts, err = ParseOptions([]string{"--connect-backend", "test", "--sessions", "--connect-token", "token"})
|
||||||
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package command
|
package command
|
||||||
|
|
||||||
const VERSION = "0.9.3"
|
const (
|
||||||
|
Version = "0.11.2"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
GitCommit string
|
GitCommit string
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ import (
|
|||||||
"github.com/sosedoff/pgweb/pkg/command"
|
"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) {
|
func currentUser() (string, error) {
|
||||||
u, err := user.Current()
|
u, err := user.Current()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -22,44 +29,73 @@ func currentUser() (string, error) {
|
|||||||
return name, nil
|
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
|
url := opts.Url
|
||||||
|
|
||||||
// Make sure to only accept urls in a standard format
|
// Validate connection string prefix
|
||||||
if !strings.HasPrefix(url, "postgres://") && !strings.HasPrefix(url, "postgresql://") {
|
if !hasValidPrefix(url) {
|
||||||
return "", errors.New("Invalid URL. Valid format: postgres://user:password@host:port/db?sslmode=mode")
|
return "", errInvalidURLFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special handling for local connections
|
// Validate the URL
|
||||||
if strings.Contains(url, "localhost") || strings.Contains(url, "127.0.0.1") {
|
uri, err := neturl.Parse(url)
|
||||||
if !strings.Contains(url, "?sslmode") {
|
if err != nil {
|
||||||
if opts.Ssl == "" {
|
return "", errInvalidURLFormat
|
||||||
url += fmt.Sprintf("?sslmode=%s", "disable")
|
}
|
||||||
} else {
|
|
||||||
url += fmt.Sprintf("?sslmode=%s", opts.Ssl)
|
// 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
|
// Rebuild query params
|
||||||
// in the connection string.
|
query := neturl.Values{}
|
||||||
if !strings.Contains(url, "?sslmode") && opts.Ssl != "" {
|
for k, v := range params {
|
||||||
url += fmt.Sprintf("?sslmode=%s", opts.Ssl)
|
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 {
|
func IsBlank(opts command.Options) bool {
|
||||||
return opts.Host == "" && opts.User == "" && opts.DbName == "" && opts.Url == ""
|
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 != "" {
|
if opts.Url != "" {
|
||||||
return FormatUrl(opts)
|
return FormatURL(opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to detect user from current OS user
|
// 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
|
// Disable ssl for localhost connections, most users have it disabled
|
||||||
if opts.Host == "localhost" || opts.Host == "127.0.0.1" {
|
if opts.Ssl == "" && (opts.Host == "localhost" || opts.Host == "127.0.0.1") {
|
||||||
if opts.Ssl == "" {
|
opts.Ssl = "disable"
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
query := neturl.Values{}
|
||||||
if opts.Ssl != "" {
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"os/user"
|
"os/user"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -13,12 +14,13 @@ func Test_Invalid_Url(t *testing.T) {
|
|||||||
opts := command.Options{}
|
opts := command.Options{}
|
||||||
examples := []string{
|
examples := []string{
|
||||||
"postgre://foobar",
|
"postgre://foobar",
|
||||||
|
"tcp://blah",
|
||||||
"foobar",
|
"foobar",
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, val := range examples {
|
for _, val := range examples {
|
||||||
opts.Url = val
|
opts.Url = val
|
||||||
str, err := BuildString(opts)
|
str, err := BuildStringFromOptions(opts)
|
||||||
|
|
||||||
assert.Equal(t, "", str)
|
assert.Equal(t, "", str)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
@@ -28,14 +30,14 @@ func Test_Invalid_Url(t *testing.T) {
|
|||||||
|
|
||||||
func Test_Valid_Url(t *testing.T) {
|
func Test_Valid_Url(t *testing.T) {
|
||||||
url := "postgres://myhost/database"
|
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, nil, err)
|
||||||
assert.Equal(t, url, str)
|
assert.Equal(t, url, str)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_Url_And_Ssl_Flag(t *testing.T) {
|
func Test_Url_And_Ssl_Flag(t *testing.T) {
|
||||||
str, err := BuildString(command.Options{
|
str, err := BuildStringFromOptions(command.Options{
|
||||||
Url: "postgres://myhost/database",
|
Url: "postgres://myhost/database",
|
||||||
Ssl: "disable",
|
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) {
|
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",
|
Url: "postgres://localhost/database",
|
||||||
})
|
})
|
||||||
|
|
||||||
assert.Equal(t, nil, err)
|
assert.Equal(t, nil, err)
|
||||||
assert.Equal(t, "postgres://localhost/database?sslmode=disable", str)
|
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",
|
Url: "postgres://127.0.0.1/database",
|
||||||
})
|
})
|
||||||
|
|
||||||
assert.Equal(t, nil, err)
|
assert.Equal(t, nil, err)
|
||||||
assert.Equal(t, "postgres://127.0.0.1/database?sslmode=disable", str)
|
assert.Equal(t, "postgres://127.0.0.1/database?sslmode=disable", str)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_Localhost_Url_And_Ssl_Flag(t *testing.T) {
|
func Test_Localhost_Url_And_Ssl_Flag(t *testing.T) {
|
||||||
str, err := BuildString(command.Options{
|
str, err := BuildStringFromOptions(command.Options{
|
||||||
Url: "postgres://localhost/database",
|
Url: "postgres://localhost/database",
|
||||||
Ssl: "require",
|
Ssl: "require",
|
||||||
})
|
})
|
||||||
|
|
||||||
assert.Equal(t, nil, err)
|
assert.Equal(t, nil, err)
|
||||||
assert.Equal(t, "postgres://localhost/database?sslmode=require", str)
|
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",
|
Url: "postgres://127.0.0.1/database",
|
||||||
Ssl: "require",
|
Ssl: "require",
|
||||||
})
|
})
|
||||||
|
|
||||||
assert.Equal(t, nil, err)
|
assert.Equal(t, nil, err)
|
||||||
assert.Equal(t, "postgres://127.0.0.1/database?sslmode=require", str)
|
assert.Equal(t, "postgres://127.0.0.1/database?sslmode=require", str)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_Localhost_Url_And_Ssl_Arg(t *testing.T) {
|
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",
|
Url: "postgres://localhost/database?sslmode=require",
|
||||||
})
|
})
|
||||||
|
|
||||||
assert.Equal(t, nil, err)
|
assert.Equal(t, nil, err)
|
||||||
assert.Equal(t, "postgres://localhost/database?sslmode=require", str)
|
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",
|
Url: "postgres://127.0.0.1/database?sslmode=require",
|
||||||
})
|
})
|
||||||
|
|
||||||
assert.Equal(t, nil, err)
|
assert.Equal(t, nil, err)
|
||||||
assert.Equal(t, "postgres://127.0.0.1/database?sslmode=require", str)
|
assert.Equal(t, "postgres://127.0.0.1/database?sslmode=require", str)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_Flag_Args(t *testing.T) {
|
func Test_Flag_Args(t *testing.T) {
|
||||||
str, err := BuildString(command.Options{
|
str, err := BuildStringFromOptions(command.Options{
|
||||||
Host: "host",
|
Host: "host",
|
||||||
Port: 5432,
|
Port: 5432,
|
||||||
User: "user",
|
User: "user",
|
||||||
@@ -116,12 +112,12 @@ func Test_Localhost(t *testing.T) {
|
|||||||
DbName: "db",
|
DbName: "db",
|
||||||
}
|
}
|
||||||
|
|
||||||
str, err := BuildString(opts)
|
str, err := BuildStringFromOptions(opts)
|
||||||
assert.Equal(t, nil, err)
|
assert.Equal(t, nil, err)
|
||||||
assert.Equal(t, "postgres://user:password@localhost:5432/db?sslmode=disable", str)
|
assert.Equal(t, "postgres://user:password@localhost:5432/db?sslmode=disable", str)
|
||||||
|
|
||||||
opts.Host = "127.0.0.1"
|
opts.Host = "127.0.0.1"
|
||||||
str, err = BuildString(opts)
|
str, err = BuildStringFromOptions(opts)
|
||||||
assert.Equal(t, nil, err)
|
assert.Equal(t, nil, err)
|
||||||
assert.Equal(t, "postgres://user:password@127.0.0.1:5432/db?sslmode=disable", str)
|
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",
|
Ssl: "require",
|
||||||
}
|
}
|
||||||
|
|
||||||
str, err := BuildString(opts)
|
str, err := BuildStringFromOptions(opts)
|
||||||
assert.Equal(t, nil, err)
|
assert.Equal(t, nil, err)
|
||||||
assert.Equal(t, "postgres://user:password@localhost:5432/db?sslmode=require", str)
|
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) {
|
func Test_No_User(t *testing.T) {
|
||||||
opts := command.Options{Host: "host", Port: 5432, DbName: "db"}
|
opts := command.Options{Host: "host", Port: 5432, DbName: "db"}
|
||||||
u, _ := user.Current()
|
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, 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) {
|
func Test_Port(t *testing.T) {
|
||||||
opts := command.Options{Host: "host", User: "user", Port: 5000, DbName: "db"}
|
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, 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) {
|
func Test_Blank(t *testing.T) {
|
||||||
|
|||||||
@@ -7,10 +7,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check if the TCP port available on localhost
|
// IsPortAvailable returns true if there's no listeners on a given port
|
||||||
func portAvailable(port int) bool {
|
func IsPortAvailable(port int) bool {
|
||||||
conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%v", port))
|
conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%v", port))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Index(err.Error(), "connection refused") > 0 {
|
if strings.Index(err.Error(), "connection refused") > 0 {
|
||||||
return true
|
return true
|
||||||
@@ -22,10 +21,10 @@ func portAvailable(port int) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get available TCP port on localhost by trying available ports in a range
|
// FindAvailablePort returns the first available TCP port in the range
|
||||||
func AvailablePort(start int, limit int) (int, error) {
|
func FindAvailablePort(start int, limit int) (int, error) {
|
||||||
for i := start; i <= (start + limit); i++ {
|
for i := start; i <= (start + limit); i++ {
|
||||||
if portAvailable(i) {
|
if IsPortAvailable(i) {
|
||||||
return i, nil
|
return i, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-12
@@ -10,14 +10,14 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_portAvailable(t *testing.T) {
|
func TestIsPortAvailable(t *testing.T) {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
t.Skip("FIXME")
|
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 {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "Unable to start test tcp listener:", err)
|
fmt.Fprintln(os.Stderr, "Unable to start test tcp listener:", err)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
@@ -35,20 +35,20 @@ func Test_portAvailable(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
assert.Equal(t, false, portAvailable(8081))
|
assert.Equal(t, false, IsPortAvailable(30000))
|
||||||
assert.Equal(t, true, portAvailable(8082))
|
assert.Equal(t, true, IsPortAvailable(30001))
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_getAvailablePort(t *testing.T) {
|
func TestFindAvailablePort(t *testing.T) {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
t.Skip("FIXME")
|
t.Skip("FIXME")
|
||||||
}
|
}
|
||||||
|
|
||||||
port, err := AvailablePort(8081, 1)
|
port, err := FindAvailablePort(30000, 1)
|
||||||
assert.Equal(t, nil, err)
|
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 {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "Unable to start test tcp listener:", err)
|
fmt.Fprintln(os.Stderr, "Unable to start test tcp listener:", err)
|
||||||
t.Fail()
|
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.EqualError(t, err, "No available port")
|
||||||
assert.Equal(t, -1, port)
|
assert.Equal(t, -1, port)
|
||||||
|
|
||||||
port, err = AvailablePort(8081, 1)
|
port, err = FindAvailablePort(30000, 1)
|
||||||
assert.Equal(t, nil, err)
|
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
|
package statements
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// ---------------------------------------------------------------------------
|
Databases = `
|
||||||
|
|
||||||
PG_DATABASES = `
|
|
||||||
SELECT
|
SELECT
|
||||||
datname
|
datname
|
||||||
FROM
|
FROM
|
||||||
@@ -15,7 +13,7 @@ ORDER BY
|
|||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
PG_SCHEMAS = `
|
Schemas = `
|
||||||
SELECT
|
SELECT
|
||||||
schema_name
|
schema_name
|
||||||
FROM
|
FROM
|
||||||
@@ -25,7 +23,7 @@ ORDER BY
|
|||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
PG_INFO = `
|
Info = `
|
||||||
SELECT
|
SELECT
|
||||||
session_user,
|
session_user,
|
||||||
current_user,
|
current_user,
|
||||||
@@ -35,11 +33,22 @@ SELECT
|
|||||||
inet_client_port(),
|
inet_client_port(),
|
||||||
inet_server_addr(),
|
inet_server_addr(),
|
||||||
inet_server_port(),
|
inet_server_port(),
|
||||||
version()`
|
version()`
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
PG_TABLE_INDEXES = `
|
EstimatedTableRowCount = `
|
||||||
|
SELECT
|
||||||
|
reltuples
|
||||||
|
FROM
|
||||||
|
pg_class
|
||||||
|
WHERE
|
||||||
|
oid = ('"' || $1::text || '"."' || $2::text || '"')::regclass
|
||||||
|
`
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TableIndexes = `
|
||||||
SELECT
|
SELECT
|
||||||
indexname, indexdef
|
indexname, indexdef
|
||||||
FROM
|
FROM
|
||||||
@@ -50,9 +59,10 @@ WHERE
|
|||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
PG_TABLE_CONSTRAINTS = `
|
TableConstraints = `
|
||||||
SELECT
|
SELECT
|
||||||
pg_get_constraintdef(c.oid, true) as condef
|
conname as name,
|
||||||
|
pg_get_constraintdef(c.oid, true) as definition
|
||||||
FROM
|
FROM
|
||||||
pg_constraint c
|
pg_constraint c
|
||||||
JOIN
|
JOIN
|
||||||
@@ -67,23 +77,31 @@ ORDER BY
|
|||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
PG_TABLE_INFO = `
|
TableInfo = `
|
||||||
SELECT
|
SELECT
|
||||||
pg_size_pretty(pg_table_size($1)) AS data_size,
|
pg_size_pretty(pg_table_size($1)) AS data_size,
|
||||||
pg_size_pretty(pg_indexes_size($1)) AS index_size,
|
pg_size_pretty(pg_indexes_size($1)) AS index_size,
|
||||||
pg_size_pretty(pg_total_relation_size($1)) AS total_size,
|
pg_size_pretty(pg_total_relation_size($1)) AS total_size,
|
||||||
(SELECT reltuples FROM pg_class WHERE oid = $1::regclass) AS rows_count`
|
(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
|
SELECT
|
||||||
column_name,
|
column_name,
|
||||||
data_type,
|
data_type,
|
||||||
is_nullable,
|
is_nullable,
|
||||||
character_maximum_length,
|
character_maximum_length,
|
||||||
character_set_catalog,
|
character_set_catalog,
|
||||||
column_default
|
column_default,
|
||||||
|
pg_catalog.col_description(('"' || $1::text || '"."' || $2::text || '"')::regclass::oid, ordinal_position) as comment
|
||||||
FROM
|
FROM
|
||||||
information_schema.columns
|
information_schema.columns
|
||||||
WHERE
|
WHERE
|
||||||
@@ -92,7 +110,7 @@ WHERE
|
|||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
PG_MATERIALIZED_VIEW_SCHEMA = `
|
MaterializedView = `
|
||||||
SELECT
|
SELECT
|
||||||
attname as column_name,
|
attname as column_name,
|
||||||
atttypid::regtype AS data_type,
|
atttypid::regtype AS data_type,
|
||||||
@@ -109,26 +127,7 @@ WHERE
|
|||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
PG_ACTIVITY = `
|
Objects = `
|
||||||
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 = `
|
|
||||||
SELECT
|
SELECT
|
||||||
n.nspname as "schema",
|
n.nspname as "schema",
|
||||||
c.relname as "name",
|
c.relname as "name",
|
||||||
@@ -141,7 +140,8 @@ SELECT
|
|||||||
WHEN 's' THEN 'special'
|
WHEN 's' THEN 'special'
|
||||||
WHEN 'f' THEN 'foreign_table'
|
WHEN 'f' THEN 'foreign_table'
|
||||||
END as "type",
|
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
|
FROM
|
||||||
pg_catalog.pg_class c
|
pg_catalog.pg_class c
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
@@ -149,6 +149,19 @@ LEFT JOIN
|
|||||||
WHERE
|
WHERE
|
||||||
c.relkind IN ('r','v','m','S','s','') AND
|
c.relkind IN ('r','v','m','S','s','') AND
|
||||||
n.nspname !~ '^pg_toast' 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`
|
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
|
#!/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
|
set -e
|
||||||
|
|
||||||
export PGHOST=${PGHOST:-192.168.99.100}
|
export PGHOST=${PGHOST:-localhost}
|
||||||
export PGUSER="postgres"
|
export PGUSER="postgres"
|
||||||
export PGPASSWORD=""
|
export PGPASSWORD=""
|
||||||
export PGDATABASE="booktown"
|
export PGDATABASE="booktown"
|
||||||
export PGPORT="15432"
|
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
|
do
|
||||||
export PGVERSION="9.$i"
|
export PGVERSION="$i"
|
||||||
|
|
||||||
|
echo "------------------------------- BEGIN TEST -------------------------------"
|
||||||
echo "Running tests against PostgreSQL v$PGVERSION"
|
echo "Running tests against PostgreSQL v$PGVERSION"
|
||||||
docker rm -f postgres || true
|
docker rm -f postgres || true
|
||||||
docker run -p $PGPORT:5432 --name postgres -e POSTGRES_PASSWORD=$PGPASSWORD -d postgres:$PGVERSION
|
docker run -p $PGPORT:5432 --name postgres -e POSTGRES_PASSWORD=$PGPASSWORD -d postgres:$PGVERSION
|
||||||
sleep 5
|
sleep 5
|
||||||
make test
|
make test
|
||||||
echo "----------"
|
echo "-------------------------------- END TEST --------------------------------"
|
||||||
done
|
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;
|
line-height: 33px;
|
||||||
height: 33px;
|
height: 33px;
|
||||||
padding: 0px 8px;
|
padding: 0px 8px;
|
||||||
padding-left: 12px;
|
|
||||||
color: #555;
|
color: #555;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar .title.main {
|
#sidebar .title.main {
|
||||||
line-height: 50px;
|
|
||||||
height: 50px;
|
height: 50px;
|
||||||
|
line-height: 50px;
|
||||||
|
font-size: 14px;
|
||||||
background: #79589f;
|
background: #79589f;
|
||||||
color: #fff;
|
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 {
|
#sidebar div.tables-list {
|
||||||
@@ -110,7 +154,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#sidebar div.tables-list .title span.current-database {
|
#sidebar div.tables-list .title span.current-database {
|
||||||
margin-left: 4px;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar div.tables-list .title:hover span.refresh {
|
#sidebar div.tables-list .title:hover span.refresh {
|
||||||
@@ -118,8 +162,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#sidebar div.tables-list .title span.refresh {
|
#sidebar div.tables-list .title span.refresh {
|
||||||
float: right;
|
position: absolute;
|
||||||
margin-right: 4px;
|
top: 0px;
|
||||||
|
right: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
display: none;
|
display: none;
|
||||||
@@ -195,6 +240,16 @@
|
|||||||
border-bottom: solid 1px #ddd;
|
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 {
|
#input .actions .btn {
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
@@ -377,11 +432,21 @@
|
|||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
color: #333;
|
color: #333;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
-webkit-font-smoothing: antialiased;
|
-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 {
|
#results th.active {
|
||||||
background: #f3f3f3;
|
background: #f3faff;
|
||||||
}
|
}
|
||||||
|
|
||||||
#results[data-mode="browse"] th:hover {
|
#results[data-mode="browse"] th:hover {
|
||||||
@@ -475,6 +540,7 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#edit_connection, #close_connection {
|
#edit_connection, #close_connection {
|
||||||
@@ -505,14 +571,32 @@
|
|||||||
margin-top: 50px;
|
margin-top: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connection-settings h1 {
|
.connection-settings .header {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-settings .header h1 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-shadow: 0px 1px 0px #fff;
|
text-shadow: 0px 1px 0px #fff;
|
||||||
margin-bottom: 25px;
|
|
||||||
color: #999;
|
color: #999;
|
||||||
font-weight: normal;
|
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 {
|
.connection-settings form {
|
||||||
background: #f6f6f6;
|
background: #f6f6f6;
|
||||||
padding: 25px;
|
padding: 25px;
|
||||||
@@ -525,6 +609,10 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.connection-settings form .no-left-padding {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.connection-scheme-group {
|
.connection-scheme-group {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -573,6 +661,14 @@
|
|||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
padding: 0px 8px;
|
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 {
|
.schema .schema-container .schema-group ul {
|
||||||
@@ -635,4 +731,4 @@
|
|||||||
|
|
||||||
.ace_active-line {
|
.ace_active-line {
|
||||||
background: none !important;
|
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.js"></script>
|
||||||
<script type="text/javascript" src="static/js/ace-pgsql.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/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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="main">
|
<div id="main">
|
||||||
@@ -23,7 +26,7 @@
|
|||||||
<li id="table_structure">Structure</li>
|
<li id="table_structure">Structure</li>
|
||||||
<li id="table_indexes">Indexes</li>
|
<li id="table_indexes">Indexes</li>
|
||||||
<li id="table_constraints">Constraints</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_history">History</li>
|
||||||
<li id="table_activity">Activity</li>
|
<li id="table_activity">Activity</li>
|
||||||
<li id="table_connection">Connection</li>
|
<li id="table_connection">Connection</li>
|
||||||
@@ -37,8 +40,9 @@
|
|||||||
<div id="sidebar">
|
<div id="sidebar">
|
||||||
<div class="tables-list">
|
<div class="tables-list">
|
||||||
<div class="wrap">
|
<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>
|
<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>
|
<span class="refresh" id="refresh_tables" title="Refresh tables list"><i class="fa fa-refresh"></i></span>
|
||||||
</div>
|
</div>
|
||||||
<div id="objects"></div>
|
<div id="objects"></div>
|
||||||
@@ -65,6 +69,7 @@
|
|||||||
<input type="button" id="explain" value="Explain Query" class="btn btn-sm btn-default" />
|
<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 id="query_progress">Please wait, query is executing...</div>
|
||||||
<div class="pull-right">
|
<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="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="csv" value="CSV" class="btn btn-sm btn-default" />
|
||||||
<input type="button" id="xml" value="XML" 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>
|
||||||
<div id="output">
|
<div id="output">
|
||||||
<div class="wrapper">
|
<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>
|
</div>
|
||||||
<div id="pagination">
|
<div id="pagination">
|
||||||
@@ -94,7 +102,7 @@
|
|||||||
<option value="null">IS NULL</option>
|
<option value="null">IS NULL</option>
|
||||||
<option value="not_null">NOT NULL</option>
|
<option value="not_null">NOT NULL</option>
|
||||||
</select>
|
</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-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>
|
<button class="btn btn-default btn-sm reset-filters"><i class="fa fa-times"></i></button>
|
||||||
</form>
|
</form>
|
||||||
@@ -112,7 +120,11 @@
|
|||||||
|
|
||||||
<div id="connection_window">
|
<div id="connection_window">
|
||||||
<div class="connection-settings">
|
<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">
|
<form role="form" class="form-horizontal" id="connection_form">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
@@ -129,8 +141,11 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<label>Enter server URL scheme</label>
|
<label>Enter server URL scheme</label>
|
||||||
<input type="text" class="form-control" id="connection_url" name="url">
|
<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</p>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -145,9 +160,12 @@
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-sm-3 control-label">Host</label>
|
<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" />
|
<input type="text" id="pg_host" class="form-control" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-sm-2 no-left-padding">
|
||||||
|
<input type="text" id="pg_port" class="form-control" placeholder="5432" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -172,14 +190,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-sm-3 control-label">Port</label>
|
<label class="col-sm-3 control-label">SSL Mode</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>
|
|
||||||
<div class="col-sm-9">
|
<div class="col-sm-9">
|
||||||
<select class="form-control" id="connection_ssl">
|
<select class="form-control" id="connection_ssl">
|
||||||
<option value="disable">disable</option>
|
<option value="disable">disable</option>
|
||||||
@@ -191,11 +202,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="connection-ssh-group">
|
<div class="connection-ssh-group">
|
||||||
|
<hr/>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-sm-3 control-label">SSH Host</label>
|
<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" />
|
<input type="text" id="ssh_host" class="form-control" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-sm-2 no-left-padding">
|
||||||
|
<input type="text" id="ssh_port" class="form-control" placeholder="22" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -213,9 +229,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<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">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -233,12 +249,45 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="tables_context_menu">
|
<div id="tables_context_menu">
|
||||||
<ul class="dropdown-menu" role="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="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="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="export" data-format="xml">Export to XML</a></li>
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
<li><a href="#" data-action="truncate">Truncate table</a></li>
|
<li><a href="#" data-action="delete">Delete View</a></li>
|
||||||
<li><a href="#" data-action="delete">Delete table</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+458
-77
@@ -17,11 +17,6 @@ var filterOptions = {
|
|||||||
"not_null": "IS NOT NULL"
|
"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() {
|
function getSessionId() {
|
||||||
var id = sessionStorage.getItem("session_id");
|
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 getObjects(cb) { apiCall("get", "/objects", {}, cb); }
|
||||||
function getTables(cb) { apiCall("get", "/tables", {}, cb); }
|
function getTables(cb) { apiCall("get", "/tables", {}, cb); }
|
||||||
function getTableRows(table, opts, cb) { apiCall("get", "/tables/" + table + "/rows", opts, 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 disconnect(cb) { apiCall("post", "/disconnect", {}, cb); }
|
||||||
|
|
||||||
function encodeQuery(query) {
|
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) {
|
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-name'><i class='fa fa-folder-o'></i><i class='fa fa-folder-open-o'></i> " + name + "</div>";
|
||||||
section += "<div class='schema-container'>";
|
section += "<div class='schema-container'>";
|
||||||
|
|
||||||
for (group of ["table", "view", "materialized_view", "sequence"]) {
|
["table", "view", "materialized_view", "sequence"].forEach(function(group) {
|
||||||
if (objects[group].length == 0) continue;
|
|
||||||
|
|
||||||
group_klass = "";
|
group_klass = "";
|
||||||
if (name == "public" && group == "table") group_klass = "expanded";
|
if (name == "public" && group == "table") group_klass = "expanded";
|
||||||
|
|
||||||
section += "<div class='schema-group " + group_klass + "'>";
|
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 += "<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>"
|
section += "<ul data-group='" + group + "'>";
|
||||||
|
|
||||||
for (item of objects[group]) {
|
if (objects[group]) {
|
||||||
var id = name + "." + item;
|
objects[group].forEach(function(item) {
|
||||||
section += "<li class='schema-" + group + "' data-type='" + group + "' data-id='" + id + "'>" + icons[group] + " " + item + "</li>";
|
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>";
|
section += "</div></div>";
|
||||||
|
|
||||||
@@ -149,6 +145,15 @@ function loadSchemas() {
|
|||||||
$("#objects").html("");
|
$("#objects").html("");
|
||||||
|
|
||||||
getObjects(function(data) {
|
getObjects(function(data) {
|
||||||
|
if (Object.keys(data).length == 0) {
|
||||||
|
data["public"] = {
|
||||||
|
table: [],
|
||||||
|
view: [],
|
||||||
|
materialized_view: [],
|
||||||
|
sequence: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
for (schema in data) {
|
for (schema in data) {
|
||||||
$(buildSchemaSection(schema, data[schema])).appendTo("#objects");
|
$(buildSchemaSection(schema, data[schema])).appendTo("#objects");
|
||||||
}
|
}
|
||||||
@@ -181,10 +186,12 @@ function getCurrentObject() {
|
|||||||
|
|
||||||
function resetTable() {
|
function resetTable() {
|
||||||
$("#results").
|
$("#results").
|
||||||
attr("data-mode", "").
|
data("mode", "").
|
||||||
text("").
|
|
||||||
removeClass("empty").
|
removeClass("empty").
|
||||||
removeClass("no-crop");
|
removeClass("no-crop");
|
||||||
|
|
||||||
|
$("#results_header").html("");
|
||||||
|
$("#results_body").html("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function performTableAction(table, action, el) {
|
function performTableAction(table, action, el) {
|
||||||
@@ -209,12 +216,60 @@ function performTableAction(table, action, el) {
|
|||||||
break;
|
break;
|
||||||
case "export":
|
case "export":
|
||||||
var format = el.data("format");
|
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 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");
|
var win = window.open(url, "_blank");
|
||||||
win.focus();
|
win.focus();
|
||||||
break;
|
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();
|
resetTable();
|
||||||
|
|
||||||
if (results.error) {
|
if (results.error) {
|
||||||
$("<tr><td>ERROR: " + results.error + "</tr></tr>").appendTo("#results");
|
$("#results_header").html("");
|
||||||
$("#results").addClass("empty");
|
$("#results_body").html("<tr><td>ERROR: " + results.error + "</tr></tr>");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (results.rows.length == 0) {
|
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");
|
$("#results").addClass("empty");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -249,25 +309,55 @@ function buildTable(results, sortColumn, sortOrder) {
|
|||||||
|
|
||||||
results.columns.forEach(function(col) {
|
results.columns.forEach(function(col) {
|
||||||
if (col === sortColumn) {
|
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 {
|
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) {
|
results.rows.forEach(function(row) {
|
||||||
var r = "";
|
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>";
|
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) {
|
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");
|
$("#nav ul li.selected").removeClass("selected");
|
||||||
$("#" + id).addClass("selected");
|
$("#" + id).addClass("selected");
|
||||||
|
|
||||||
|
// Persist tab selection into the session storage
|
||||||
|
sessionStorage.setItem("tab", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showQueryHistory() {
|
function showQueryHistory() {
|
||||||
@@ -406,16 +496,30 @@ function showTableContent(sortColumn, sortOrder) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getTableRows(name, opts, function(data) {
|
getTableRows(name, opts, function(data) {
|
||||||
$("#results").attr("data-mode", "browse");
|
|
||||||
$("#input").hide();
|
$("#input").hide();
|
||||||
$("#body").prop("class", "with-pagination");
|
$("#body").prop("class", "with-pagination");
|
||||||
|
|
||||||
buildTable(data, sortColumn, sortOrder);
|
buildTable(data, sortColumn, sortOrder);
|
||||||
setCurrentTab("table_content");
|
setCurrentTab("table_content");
|
||||||
updatePaginator(data.pagination);
|
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() {
|
function showTableStructure() {
|
||||||
var name = getCurrentObject().name;
|
var name = getCurrentObject().name;
|
||||||
|
|
||||||
@@ -425,12 +529,10 @@ function showTableStructure() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setCurrentTab("table_structure");
|
setCurrentTab("table_structure");
|
||||||
|
|
||||||
$("#input").hide();
|
$("#input").hide();
|
||||||
$("#body").prop("class", "full");
|
$("#body").prop("class", "full");
|
||||||
|
|
||||||
console.log(getCurrentObject());
|
|
||||||
|
|
||||||
getTableStructure(name, { type: getCurrentObject().type }, function(data) {
|
getTableStructure(name, { type: getCurrentObject().type }, function(data) {
|
||||||
buildTable(data);
|
buildTable(data);
|
||||||
$("#results").addClass("no-crop");
|
$("#results").addClass("no-crop");
|
||||||
@@ -438,6 +540,10 @@ function showTableStructure() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showQueryPanel() {
|
function showQueryPanel() {
|
||||||
|
if (!$("#table_query").hasClass("selected")) {
|
||||||
|
resetTable();
|
||||||
|
}
|
||||||
|
|
||||||
setCurrentTab("table_query");
|
setCurrentTab("table_query");
|
||||||
editor.focus();
|
editor.focus();
|
||||||
|
|
||||||
@@ -466,10 +572,18 @@ function showConnectionPanel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showActivityPanel() {
|
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) {
|
apiCall("get", "/activity", {}, function(data) {
|
||||||
buildTable(data);
|
buildTable(data, null, null, options);
|
||||||
$("#input").hide();
|
$("#input").hide();
|
||||||
$("#body").addClass("full");
|
$("#body").addClass("full");
|
||||||
});
|
});
|
||||||
@@ -496,15 +610,14 @@ function runQuery() {
|
|||||||
$("#query_progress").hide();
|
$("#query_progress").hide();
|
||||||
$("#input").show();
|
$("#input").show();
|
||||||
$("#body").removeClass("full");
|
$("#body").removeClass("full");
|
||||||
|
$("#results").data("mode", "query");
|
||||||
|
|
||||||
if (query.toLowerCase().indexOf("explain") != -1) {
|
if (query.toLowerCase().indexOf("explain") != -1) {
|
||||||
$("#results").addClass("no-crop");
|
$("#results").addClass("no-crop");
|
||||||
}
|
}
|
||||||
|
|
||||||
var re = /(create|drop)\s/i;
|
|
||||||
|
|
||||||
// Reload objects list if anything was created/deleted
|
// Reload objects list if anything was created/deleted
|
||||||
if (query.match(re)) {
|
if (query.match(/(create|drop)\s/i)) {
|
||||||
loadSchemas();
|
loadSchemas();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -516,7 +629,7 @@ function runExplain() {
|
|||||||
$("#run, #explain, #csv, #json, #xml").prop("disabled", true);
|
$("#run, #explain, #csv, #json, #xml").prop("disabled", true);
|
||||||
$("#query_progress").show();
|
$("#query_progress").show();
|
||||||
|
|
||||||
var query = $.trim(editor.getValue());
|
var query = $.trim(editor.getSelectedText() || editor.getValue());
|
||||||
|
|
||||||
if (query.length == 0) {
|
if (query.length == 0) {
|
||||||
$("#run, #explain, #csv, #json, #xml").prop("disabled", false);
|
$("#run, #explain, #csv, #json, #xml").prop("disabled", false);
|
||||||
@@ -536,19 +649,49 @@ function runExplain() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function exportTo(format) {
|
function exportTo(format) {
|
||||||
var query = $.trim(editor.getValue());
|
var query = $.trim(editor.getSelectedText() || editor.getValue());
|
||||||
|
|
||||||
if (query.length == 0) {
|
if (query.length == 0) {
|
||||||
return;
|
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');
|
var win = window.open(url, '_blank');
|
||||||
|
|
||||||
setCurrentTab("table_query");
|
setCurrentTab("table_query");
|
||||||
win.focus();
|
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) {
|
function buildTableFilters(name, type) {
|
||||||
getTableStructure(name, { type: type }, function(data) {
|
getTableStructure(name, { type: type }, function(data) {
|
||||||
if (data.rows.length == 0) {
|
if (data.rows.length == 0) {
|
||||||
@@ -560,7 +703,9 @@ function buildTableFilters(name, type) {
|
|||||||
|
|
||||||
$("#pagination select.column").html("<option value='' selected>Select column</option>");
|
$("#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]);
|
var el = $("<option/>").attr("value", row[0]).text(row[0]);
|
||||||
$("#pagination select.column").append(el);
|
$("#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() {
|
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) {
|
getBookmarks(function(data) {
|
||||||
// Do not add any bookmarks if we've got an error
|
// Do not add any bookmarks if we've got an error
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
@@ -687,18 +861,162 @@ function getConnectionString() {
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindContextMenus() {
|
// Add a context menu to the results table header columns
|
||||||
$(".schema-group ul").each(function(id, el) {
|
function bindTableHeaderMenu() {
|
||||||
$(el).contextmenu({
|
$("#results_header").contextmenu({
|
||||||
target: "#tables_context_menu",
|
scopes: "th",
|
||||||
scopes: "li.schema-table",
|
target: "#results_header_menu",
|
||||||
onItem: function(context, e) {
|
before: function(e, element, target) {
|
||||||
var el = $(e.target);
|
// Enable menu for browsing table rows view only.
|
||||||
var table = $(context[0]).data("id");
|
if ($("#results").data("mode") != "browse") {
|
||||||
var action = el.data("action");
|
e.preventDefault();
|
||||||
performTableAction(table, action, el);
|
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");
|
exportTo("xml");
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#results").on("click", "tr", function() {
|
$("#results").on("click", "tr", function(e) {
|
||||||
$("#results tr.selected").removeClass();
|
$("#results tr.selected").removeClass();
|
||||||
$(this).addClass("selected");
|
$(this).addClass("selected");
|
||||||
});
|
});
|
||||||
@@ -757,25 +1075,42 @@ $(document).ready(function() {
|
|||||||
$(".filters select, .filters input").val("");
|
$(".filters select, .filters input").val("");
|
||||||
|
|
||||||
showTableInfo();
|
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) {
|
$("#results").on("click", "th", function(e) {
|
||||||
var sortColumn = this.attributes['data'].value;
|
if (!$("#table_content").hasClass("selected")) return;
|
||||||
var contentTab = $('#table_content').hasClass('selected');
|
|
||||||
|
|
||||||
if (!contentTab) {
|
var sortColumn = $(this).data("name");
|
||||||
return;
|
var sortOrder = $(this).data("order") === "ASC" ? "DESC" : "ASC";
|
||||||
}
|
|
||||||
|
|
||||||
if (this.dataset.sortOrder === "ASC") {
|
$(this).data("order", sortOrder);
|
||||||
this.dataset.sortOrder = "DESC"
|
showTableContent(sortColumn, sortOrder);
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.dataset.sortOrder = "ASC"
|
|
||||||
}
|
|
||||||
|
|
||||||
showTableContent(sortColumn, this.dataset.sortOrder);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#results").on("dblclick", "td > div", function() {
|
$("#results").on("dblclick", "td > div", function() {
|
||||||
@@ -844,13 +1179,21 @@ $(document).ready(function() {
|
|||||||
showTableContent();
|
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() {
|
$("#pagination .next-page").on("click", function() {
|
||||||
var current = $(".current-page").data("page");
|
var current = $(".current-page").data("page");
|
||||||
var total = $(".current-page").data("pages");
|
var total = $(".current-page").data("pages");
|
||||||
|
|
||||||
if (total > current) {
|
if (total > current) {
|
||||||
$(".current-page").data("page", current + 1);
|
$(".current-page").data("page", current + 1);
|
||||||
showTableContent();
|
showPaginatedTableContent();
|
||||||
|
|
||||||
if (current + 1 == total) {
|
if (current + 1 == total) {
|
||||||
$(this).prop("disabled", "disabled");
|
$(this).prop("disabled", "disabled");
|
||||||
@@ -868,7 +1211,7 @@ $(document).ready(function() {
|
|||||||
if (current > 1) {
|
if (current > 1) {
|
||||||
$(".current-page").data("page", current - 1);
|
$(".current-page").data("page", current - 1);
|
||||||
$(".next-page").prop("disabled", "");
|
$(".next-page").prop("disabled", "");
|
||||||
showTableContent();
|
showPaginatedTableContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current == 1) {
|
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() {
|
$("#edit_connection").on("click", function() {
|
||||||
if (connected) {
|
if (connected) {
|
||||||
$("#close_connection_window").show();
|
$("#close_connection_window").show();
|
||||||
@@ -885,8 +1248,11 @@ $(document).ready(function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#close_connection").on("click", function() {
|
$("#close_connection").on("click", function() {
|
||||||
|
if (!confirm("Are you sure you want to disconnect?")) return;
|
||||||
|
|
||||||
disconnect(function() {
|
disconnect(function() {
|
||||||
showConnectionSettings();
|
showConnectionSettings();
|
||||||
|
resetTable();
|
||||||
$("#close_connection_window").hide();
|
$("#close_connection_window").hide();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -934,15 +1300,12 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
$("#connection_bookmarks").on("change", function(e) {
|
$("#connection_bookmarks").on("change", function(e) {
|
||||||
var name = $.trim($(this).val());
|
var name = $.trim($(this).val());
|
||||||
|
if (name == "") return;
|
||||||
|
|
||||||
if (name == "") {
|
var item = bookmarks[name];
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
item = bookmarks[name];
|
|
||||||
|
|
||||||
// Check if bookmark only has url set
|
// Check if bookmark only has url set
|
||||||
if (item.url != "") {
|
if (item.url && item.url != "") {
|
||||||
$("#connection_url").val(item.url);
|
$("#connection_url").val(item.url);
|
||||||
$("#connection_scheme").click();
|
$("#connection_scheme").click();
|
||||||
return;
|
return;
|
||||||
@@ -955,12 +1318,13 @@ $(document).ready(function() {
|
|||||||
$("#pg_password").val(item.password);
|
$("#pg_password").val(item.password);
|
||||||
$("#pg_db").val(item.database);
|
$("#pg_db").val(item.database);
|
||||||
$("#connection_ssl").val(item.ssl);
|
$("#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_host").val(item.ssh.host);
|
||||||
$("#ssh_port").val(item.ssh.port);
|
$("#ssh_port").val(item.ssh.port);
|
||||||
$("#ssh_user").val(item.ssh.user);
|
$("#ssh_user").val(item.ssh.user);
|
||||||
$("#ssh_password").val(item.ssh.password);
|
$("#ssh_password").val(item.ssh.password);
|
||||||
|
$("#ssh_key").val(item.ssh.key);
|
||||||
$("#connection_ssh").click();
|
$("#connection_ssh").click();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -968,7 +1332,9 @@ $(document).ready(function() {
|
|||||||
$("#ssh_port").val("");
|
$("#ssh_port").val("");
|
||||||
$("#ssh_user").val("");
|
$("#ssh_user").val("");
|
||||||
$("#ssh_password").val("");
|
$("#ssh_password").val("");
|
||||||
|
$("#ssh_key").val("");
|
||||||
$(".connection-ssh-group").hide();
|
$(".connection-ssh-group").hide();
|
||||||
|
$("#connection_standard").click();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -990,6 +1356,7 @@ $(document).ready(function() {
|
|||||||
params["ssh_port"] = $("#ssh_port").val();
|
params["ssh_port"] = $("#ssh_port").val();
|
||||||
params["ssh_user"] = $("#ssh_user").val();
|
params["ssh_user"] = $("#ssh_user").val();
|
||||||
params["ssh_password"] = $("#ssh_password").val();
|
params["ssh_password"] = $("#ssh_password").val();
|
||||||
|
params["ssh_key"] = $("#ssh_key").val();
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#connection_error").hide();
|
$("#connection_error").hide();
|
||||||
@@ -1016,10 +1383,20 @@ $(document).ready(function() {
|
|||||||
initEditor();
|
initEditor();
|
||||||
addShortcutTooltips();
|
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) {
|
apiCall("get", "/connection", {}, function(resp) {
|
||||||
if (resp.error) {
|
if (resp.error) {
|
||||||
connected = false;
|
connected = false;
|
||||||
showConnectionSettings();
|
showConnectionSettings();
|
||||||
|
$(".connection-actions").show();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
connected = true;
|
connected = true;
|
||||||
@@ -1027,6 +1404,10 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
$("#current_database").text(resp.current_database);
|
$("#current_database").text(resp.current_database);
|
||||||
$("#main").show();
|
$("#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:
|
go:
|
||||||
- 1.1
|
- 1.1
|
||||||
- 1.2
|
- 1.2
|
||||||
|
- 1.3
|
||||||
|
- 1.4
|
||||||
|
- 1.5
|
||||||
|
- 1.6
|
||||||
- tip
|
- tip
|
||||||
install:
|
install:
|
||||||
- go install ./...
|
- go install ./...
|
||||||
@@ -9,4 +13,3 @@ install:
|
|||||||
script:
|
script:
|
||||||
- export PATH="$PATH:$HOME/gopath/bin"
|
- export PATH="$PATH:$HOME/gopath/bin"
|
||||||
- make test
|
- make test
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
Compatible with TOML version
|
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 parser and encoder for Go with reflection
|
||||||
|
|
||||||
TOML stands for Tom's Obvious, Minimal Language. This Go package provides a
|
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
|
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.)
|
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
|
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:
|
Installation:
|
||||||
|
|
||||||
@@ -26,8 +26,7 @@ go get github.com/BurntSushi/toml/cmd/tomlv
|
|||||||
tomlv some-toml-file.toml
|
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
|
### Testing
|
||||||
|
|
||||||
@@ -87,7 +86,7 @@ type TOML struct {
|
|||||||
|
|
||||||
### Using the `encoding.TextUnmarshaler` interface
|
### 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:
|
`time.Duration` values:
|
||||||
|
|
||||||
```toml
|
```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:
|
`encoding.TextUnmarshaler` interface:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -217,4 +216,3 @@ Note that a case insensitive match will be tried if an exact match can't be
|
|||||||
found.
|
found.
|
||||||
|
|
||||||
A working example of the above can be found in `_examples/example.{go,toml}`.
|
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"
|
"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
|
// Unmarshaler is the interface implemented by objects that can unmarshal a
|
||||||
// TOML description of themselves.
|
// 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,
|
// This decoder will not handle cyclic types. If a cyclic type is passed,
|
||||||
// `Decode` will not terminate.
|
// `Decode` will not terminate.
|
||||||
func Decode(data string, v interface{}) (MetaData, error) {
|
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)
|
p, err := parse(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return MetaData{}, err
|
return MetaData{}, err
|
||||||
@@ -111,7 +120,7 @@ func Decode(data string, v interface{}) (MetaData, error) {
|
|||||||
p.mapping, p.types, p.ordered,
|
p.mapping, p.types, p.ordered,
|
||||||
make(map[string]bool, len(p.ordered)), nil,
|
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
|
// 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:
|
case reflect.Interface:
|
||||||
// we only support empty interfaces.
|
// we only support empty interfaces.
|
||||||
if rv.NumMethod() > 0 {
|
if rv.NumMethod() > 0 {
|
||||||
return e("Unsupported type '%s'.", rv.Kind())
|
return e("unsupported type %s", rv.Type())
|
||||||
}
|
}
|
||||||
return md.unifyAnything(data, rv)
|
return md.unifyAnything(data, rv)
|
||||||
case reflect.Float32:
|
case reflect.Float32:
|
||||||
@@ -219,13 +228,17 @@ func (md *MetaData) unify(data interface{}, rv reflect.Value) error {
|
|||||||
case reflect.Float64:
|
case reflect.Float64:
|
||||||
return md.unifyFloat64(data, rv)
|
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 {
|
func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error {
|
||||||
tmap, ok := mapping.(map[string]interface{})
|
tmap, ok := mapping.(map[string]interface{})
|
||||||
if !ok {
|
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 {
|
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.decoded[md.context.add(key).String()] = true
|
||||||
md.context = append(md.context, key)
|
md.context = append(md.context, key)
|
||||||
if err := md.unify(datum, subv); err != nil {
|
if err := md.unify(datum, subv); err != nil {
|
||||||
return e("Type mismatch for '%s.%s': %s",
|
return err
|
||||||
rv.Type().String(), f.name, err)
|
|
||||||
}
|
}
|
||||||
md.context = md.context[0 : len(md.context)-1]
|
md.context = md.context[0 : len(md.context)-1]
|
||||||
} else if f.name != "" {
|
} else if f.name != "" {
|
||||||
// Bad user! No soup for you!
|
// Bad user! No soup for you!
|
||||||
return e("Field '%s.%s' is unexported, and therefore cannot "+
|
return e("cannot write unexported field %s.%s",
|
||||||
"be loaded with reflection.", rv.Type().String(), f.name)
|
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 {
|
func (md *MetaData) unifyMap(mapping interface{}, rv reflect.Value) error {
|
||||||
tmap, ok := mapping.(map[string]interface{})
|
tmap, ok := mapping.(map[string]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
|
if tmap == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return badtype("map", mapping)
|
return badtype("map", mapping)
|
||||||
}
|
}
|
||||||
if rv.IsNil() {
|
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 {
|
func (md *MetaData) unifyArray(data interface{}, rv reflect.Value) error {
|
||||||
datav := reflect.ValueOf(data)
|
datav := reflect.ValueOf(data)
|
||||||
if datav.Kind() != reflect.Slice {
|
if datav.Kind() != reflect.Slice {
|
||||||
|
if !datav.IsValid() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return badtype("slice", data)
|
return badtype("slice", data)
|
||||||
}
|
}
|
||||||
sliceLen := datav.Len()
|
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 {
|
func (md *MetaData) unifySlice(data interface{}, rv reflect.Value) error {
|
||||||
datav := reflect.ValueOf(data)
|
datav := reflect.ValueOf(data)
|
||||||
if datav.Kind() != reflect.Slice {
|
if datav.Kind() != reflect.Slice {
|
||||||
|
if !datav.IsValid() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return badtype("slice", data)
|
return badtype("slice", data)
|
||||||
}
|
}
|
||||||
sliceLen := datav.Len()
|
n := datav.Len()
|
||||||
if rv.IsNil() {
|
if rv.IsNil() || rv.Cap() < n {
|
||||||
rv.Set(reflect.MakeSlice(rv.Type(), sliceLen, sliceLen))
|
rv.Set(reflect.MakeSlice(rv.Type(), n, n))
|
||||||
}
|
}
|
||||||
|
rv.SetLen(n)
|
||||||
return md.unifySliceArray(datav, rv)
|
return md.unifySliceArray(datav, rv)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,15 +387,15 @@ func (md *MetaData) unifyInt(data interface{}, rv reflect.Value) error {
|
|||||||
// No bounds checking necessary.
|
// No bounds checking necessary.
|
||||||
case reflect.Int8:
|
case reflect.Int8:
|
||||||
if num < math.MinInt8 || num > math.MaxInt8 {
|
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:
|
case reflect.Int16:
|
||||||
if num < math.MinInt16 || num > math.MaxInt16 {
|
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:
|
case reflect.Int32:
|
||||||
if num < math.MinInt32 || num > math.MaxInt32 {
|
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)
|
rv.SetInt(num)
|
||||||
@@ -384,15 +406,15 @@ func (md *MetaData) unifyInt(data interface{}, rv reflect.Value) error {
|
|||||||
// No bounds checking necessary.
|
// No bounds checking necessary.
|
||||||
case reflect.Uint8:
|
case reflect.Uint8:
|
||||||
if num < 0 || unum > math.MaxUint8 {
|
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:
|
case reflect.Uint16:
|
||||||
if num < 0 || unum > math.MaxUint16 {
|
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:
|
case reflect.Uint32:
|
||||||
if num < 0 || unum > math.MaxUint32 {
|
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)
|
rv.SetUint(unum)
|
||||||
@@ -458,7 +480,7 @@ func rvalue(v interface{}) reflect.Value {
|
|||||||
// interest to us (like encoding.TextUnmarshaler).
|
// interest to us (like encoding.TextUnmarshaler).
|
||||||
func indirect(v reflect.Value) reflect.Value {
|
func indirect(v reflect.Value) reflect.Value {
|
||||||
if v.Kind() != reflect.Ptr {
|
if v.Kind() != reflect.Ptr {
|
||||||
if v.CanAddr() {
|
if v.CanSet() {
|
||||||
pv := v.Addr()
|
pv := v.Addr()
|
||||||
if _, ok := pv.Interface().(TextUnmarshaler); ok {
|
if _, ok := pv.Interface().(TextUnmarshaler); ok {
|
||||||
return pv
|
return pv
|
||||||
@@ -483,10 +505,5 @@ func isUnifiable(rv reflect.Value) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func badtype(expected string, data interface{}) error {
|
func badtype(expected string, data interface{}) error {
|
||||||
return e("Expected %s but found '%T'.", expected, data)
|
return e("cannot load TOML value of type %T into a Go %s", data, expected)
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
+22
@@ -59,6 +59,28 @@ func (k Key) String() string {
|
|||||||
return strings.Join(k, ".")
|
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 {
|
func (k Key) add(piece string) Key {
|
||||||
newKey := make(Key, len(k)+1)
|
newKey := make(Key, len(k)+1)
|
||||||
copy(newKey, k)
|
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
|
the Primitive type, and querying the set of keys in a TOML document with the
|
||||||
MetaData type.
|
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
|
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
|
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 (
|
var (
|
||||||
errArrayMixedElementTypes = errors.New(
|
errArrayMixedElementTypes = errors.New(
|
||||||
"can't encode array with mixed element types")
|
"toml: cannot encode array with mixed element types")
|
||||||
errArrayNilElement = errors.New(
|
errArrayNilElement = errors.New(
|
||||||
"can't encode array with nil element")
|
"toml: cannot encode array with nil element")
|
||||||
errNonString = errors.New(
|
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(
|
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(
|
errArrayNoTable = errors.New(
|
||||||
"TOML array element can't contain a table")
|
"toml: TOML array element cannot contain a table")
|
||||||
errNoKey = errors.New(
|
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
|
errAnything = errors.New("") // used in testing
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -118,7 +118,8 @@ func (enc *Encoder) encode(key Key, rv reflect.Value) {
|
|||||||
|
|
||||||
k := rv.Kind()
|
k := rv.Kind()
|
||||||
switch k {
|
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.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
|
||||||
reflect.Uint64,
|
reflect.Uint64,
|
||||||
reflect.Float32, reflect.Float64, reflect.String, reflect.Bool:
|
reflect.Float32, reflect.Float64, reflect.String, reflect.Bool:
|
||||||
@@ -147,7 +148,7 @@ func (enc *Encoder) encode(key Key, rv reflect.Value) {
|
|||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
enc.eTable(key, rv)
|
enc.eTable(key, rv)
|
||||||
default:
|
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
|
// Special case time.Time as a primitive. Has to come before
|
||||||
// TextMarshaler below because time.Time implements
|
// TextMarshaler below because time.Time implements
|
||||||
// encoding.TextMarshaler, but we need to always use UTC.
|
// 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
|
return
|
||||||
case TextMarshaler:
|
case TextMarshaler:
|
||||||
// Special case. Use text marshaler if it's available for this value.
|
// 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() {
|
switch rv.Kind() {
|
||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
enc.wf(strconv.FormatBool(rv.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))
|
enc.wf(strconv.FormatInt(rv.Int(), 10))
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16,
|
case reflect.Uint, reflect.Uint8, reflect.Uint16,
|
||||||
reflect.Uint32, reflect.Uint64:
|
reflect.Uint32, reflect.Uint64:
|
||||||
@@ -189,7 +191,7 @@ func (enc *Encoder) eElement(rv reflect.Value) {
|
|||||||
case reflect.String:
|
case reflect.String:
|
||||||
enc.writeQuoted(rv.String())
|
enc.writeQuoted(rv.String())
|
||||||
default:
|
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 {
|
if len(key) == 0 {
|
||||||
encPanic(errNoKey)
|
encPanic(errNoKey)
|
||||||
}
|
}
|
||||||
panicIfInvalidKey(key, true)
|
|
||||||
for i := 0; i < rv.Len(); i++ {
|
for i := 0; i < rv.Len(); i++ {
|
||||||
trv := rv.Index(i)
|
trv := rv.Index(i)
|
||||||
if isNil(trv) {
|
if isNil(trv) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
panicIfInvalidKey(key)
|
||||||
enc.newline()
|
enc.newline()
|
||||||
enc.wf("%s[[%s]]", enc.indentStr(key), key.String())
|
enc.wf("%s[[%s]]", enc.indentStr(key), key.maybeQuotedAll())
|
||||||
enc.newline()
|
enc.newline()
|
||||||
enc.eMapOrStruct(key, trv)
|
enc.eMapOrStruct(key, trv)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (enc *Encoder) eTable(key Key, rv reflect.Value) {
|
func (enc *Encoder) eTable(key Key, rv reflect.Value) {
|
||||||
|
panicIfInvalidKey(key)
|
||||||
if len(key) == 1 {
|
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.)
|
// (The newline isn't written if nothing else has been written though.)
|
||||||
enc.newline()
|
enc.newline()
|
||||||
}
|
}
|
||||||
if len(key) > 0 {
|
if len(key) > 0 {
|
||||||
panicIfInvalidKey(key, true)
|
enc.wf("%s[%s]", enc.indentStr(key), key.maybeQuotedAll())
|
||||||
enc.wf("%s[%s]", enc.indentStr(key), key.String())
|
|
||||||
enc.newline()
|
enc.newline()
|
||||||
}
|
}
|
||||||
enc.eMapOrStruct(key, rv)
|
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) {
|
addFields = func(rt reflect.Type, rv reflect.Value, start []int) {
|
||||||
for i := 0; i < rt.NumField(); i++ {
|
for i := 0; i < rt.NumField(); i++ {
|
||||||
f := rt.Field(i)
|
f := rt.Field(i)
|
||||||
// skip unexporded fields
|
// skip unexported fields
|
||||||
if f.PkgPath != "" {
|
if f.PkgPath != "" && !f.Anonymous {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
frv := rv.Field(i)
|
frv := rv.Field(i)
|
||||||
if f.Anonymous {
|
if f.Anonymous {
|
||||||
frv := eindirect(frv)
|
t := f.Type
|
||||||
t := frv.Type()
|
switch t.Kind() {
|
||||||
if t.Kind() != reflect.Struct {
|
case reflect.Struct:
|
||||||
encPanic(errAnonNonStruct)
|
// 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...))
|
fieldsSub = append(fieldsSub, append(start, f.Index...))
|
||||||
} else {
|
} else {
|
||||||
fieldsDirect = append(fieldsDirect, append(start, f.Index...))
|
fieldsDirect = append(fieldsDirect, append(start, f.Index...))
|
||||||
@@ -334,13 +353,21 @@ func (enc *Encoder) eStruct(key Key, rv reflect.Value) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
keyName := sft.Tag.Get("toml")
|
opts := getOptions(sft.Tag)
|
||||||
if keyName == "-" {
|
if opts.skip {
|
||||||
continue
|
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)
|
enc.encode(key.add(keyName), sf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -348,10 +375,10 @@ func (enc *Encoder) eStruct(key Key, rv reflect.Value) {
|
|||||||
writeFields(fieldsSub)
|
writeFields(fieldsSub)
|
||||||
}
|
}
|
||||||
|
|
||||||
// tomlTypeName returns the TOML type name of the Go value's type. It is used to
|
// tomlTypeName returns the TOML type name of the Go value's type. It is
|
||||||
// determine whether the types of array elements are mixed (which is forbidden).
|
// used to determine whether the types of array elements are mixed (which is
|
||||||
// If the Go value is nil, then it is illegal for it to be an array element, and
|
// forbidden). If the Go value is nil, then it is illegal for it to be an array
|
||||||
// valueIsNil is returned as true.
|
// element, and valueIsNil is returned as true.
|
||||||
|
|
||||||
// Returns the TOML type of a Go value. The type may be `nil`, which means
|
// Returns the TOML type of a Go value. The type may be `nil`, which means
|
||||||
// no concrete TOML type could be found.
|
// no concrete TOML type could be found.
|
||||||
@@ -362,7 +389,8 @@ func tomlTypeOfGo(rv reflect.Value) tomlType {
|
|||||||
switch rv.Kind() {
|
switch rv.Kind() {
|
||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
return tomlBool
|
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.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
|
||||||
reflect.Uint64:
|
reflect.Uint64:
|
||||||
return tomlInteger
|
return tomlInteger
|
||||||
@@ -371,9 +399,8 @@ func tomlTypeOfGo(rv reflect.Value) tomlType {
|
|||||||
case reflect.Array, reflect.Slice:
|
case reflect.Array, reflect.Slice:
|
||||||
if typeEqual(tomlHash, tomlArrayType(rv)) {
|
if typeEqual(tomlHash, tomlArrayType(rv)) {
|
||||||
return tomlArrayHash
|
return tomlArrayHash
|
||||||
} else {
|
|
||||||
return tomlArray
|
|
||||||
}
|
}
|
||||||
|
return tomlArray
|
||||||
case reflect.Ptr, reflect.Interface:
|
case reflect.Ptr, reflect.Interface:
|
||||||
return tomlTypeOfGo(rv.Elem())
|
return tomlTypeOfGo(rv.Elem())
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
@@ -430,6 +457,54 @@ func tomlArrayType(rv reflect.Value) tomlType {
|
|||||||
return firstType
|
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() {
|
func (enc *Encoder) newline() {
|
||||||
if enc.hasWritten {
|
if enc.hasWritten {
|
||||||
enc.wf("\n")
|
enc.wf("\n")
|
||||||
@@ -440,8 +515,8 @@ func (enc *Encoder) keyEqElement(key Key, val reflect.Value) {
|
|||||||
if len(key) == 0 {
|
if len(key) == 0 {
|
||||||
encPanic(errNoKey)
|
encPanic(errNoKey)
|
||||||
}
|
}
|
||||||
panicIfInvalidKey(key, false)
|
panicIfInvalidKey(key)
|
||||||
enc.wf("%s%s = ", enc.indentStr(key), key[len(key)-1])
|
enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1))
|
||||||
enc.eElement(val)
|
enc.eElement(val)
|
||||||
enc.newline()
|
enc.newline()
|
||||||
}
|
}
|
||||||
@@ -479,37 +554,15 @@ func isNil(rv reflect.Value) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func panicIfInvalidKey(key Key, hash bool) {
|
func panicIfInvalidKey(key Key) {
|
||||||
if hash {
|
for _, k := range key {
|
||||||
for _, k := range key {
|
if len(k) == 0 {
|
||||||
if !isValidTableName(k) {
|
encPanic(e("Key '%s' is not a valid table name. Key names "+
|
||||||
encPanic(e("Key '%s' is not a valid table name. Table names "+
|
"cannot be empty.", key.maybeQuotedAll()))
|
||||||
"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 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 {
|
func isValidKeyName(s string) bool {
|
||||||
if len(s) == 0 {
|
return len(s) != 0
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -14,6 +14,6 @@ import (
|
|||||||
// so that Go 1.1 can be supported.
|
// so that Go 1.1 can be supported.
|
||||||
type TextMarshaler encoding.TextMarshaler
|
type TextMarshaler encoding.TextMarshaler
|
||||||
|
|
||||||
// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined here
|
// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined
|
||||||
// so that Go 1.1 can be supported.
|
// here so that Go 1.1 can be supported.
|
||||||
type TextUnmarshaler encoding.TextUnmarshaler
|
type TextUnmarshaler encoding.TextUnmarshaler
|
||||||
|
|||||||
+2
-2
@@ -11,8 +11,8 @@ type TextMarshaler interface {
|
|||||||
MarshalText() (text []byte, err error)
|
MarshalText() (text []byte, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined here
|
// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined
|
||||||
// so that Go 1.1 can be supported.
|
// here so that Go 1.1 can be supported.
|
||||||
type TextUnmarshaler interface {
|
type TextUnmarshaler interface {
|
||||||
UnmarshalText(text []byte) error
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -67,7 +66,7 @@ func parse(data string) (p *parser, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) panicf(format string, v ...interface{}) {
|
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...))
|
p.approxLine, p.current(), fmt.Sprintf(format, v...))
|
||||||
panic(parseError(msg))
|
panic(parseError(msg))
|
||||||
}
|
}
|
||||||
@@ -75,13 +74,13 @@ func (p *parser) panicf(format string, v ...interface{}) {
|
|||||||
func (p *parser) next() item {
|
func (p *parser) next() item {
|
||||||
it := p.lx.nextItem()
|
it := p.lx.nextItem()
|
||||||
if it.typ == itemError {
|
if it.typ == itemError {
|
||||||
p.panicf("Near line %d: %s", it.line, it.val)
|
p.panicf("%s", it.val)
|
||||||
}
|
}
|
||||||
return it
|
return it
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) bug(format string, v ...interface{}) {
|
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 {
|
func (p *parser) expect(typ itemType) item {
|
||||||
@@ -102,12 +101,12 @@ func (p *parser) topLevel(item item) {
|
|||||||
p.approxLine = item.line
|
p.approxLine = item.line
|
||||||
p.expect(itemText)
|
p.expect(itemText)
|
||||||
case itemTableStart:
|
case itemTableStart:
|
||||||
kg := p.expect(itemText)
|
kg := p.next()
|
||||||
p.approxLine = kg.line
|
p.approxLine = kg.line
|
||||||
|
|
||||||
key := make(Key, 0)
|
var key Key
|
||||||
for ; kg.typ == itemText; kg = p.next() {
|
for ; kg.typ != itemTableEnd && kg.typ != itemEOF; kg = p.next() {
|
||||||
key = append(key, kg.val)
|
key = append(key, p.keyString(kg))
|
||||||
}
|
}
|
||||||
p.assertEqual(itemTableEnd, kg.typ)
|
p.assertEqual(itemTableEnd, kg.typ)
|
||||||
|
|
||||||
@@ -115,12 +114,12 @@ func (p *parser) topLevel(item item) {
|
|||||||
p.setType("", tomlHash)
|
p.setType("", tomlHash)
|
||||||
p.ordered = append(p.ordered, key)
|
p.ordered = append(p.ordered, key)
|
||||||
case itemArrayTableStart:
|
case itemArrayTableStart:
|
||||||
kg := p.expect(itemText)
|
kg := p.next()
|
||||||
p.approxLine = kg.line
|
p.approxLine = kg.line
|
||||||
|
|
||||||
key := make(Key, 0)
|
var key Key
|
||||||
for ; kg.typ == itemText; kg = p.next() {
|
for ; kg.typ != itemArrayTableEnd && kg.typ != itemEOF; kg = p.next() {
|
||||||
key = append(key, kg.val)
|
key = append(key, p.keyString(kg))
|
||||||
}
|
}
|
||||||
p.assertEqual(itemArrayTableEnd, kg.typ)
|
p.assertEqual(itemArrayTableEnd, kg.typ)
|
||||||
|
|
||||||
@@ -128,29 +127,44 @@ func (p *parser) topLevel(item item) {
|
|||||||
p.setType("", tomlArrayHash)
|
p.setType("", tomlArrayHash)
|
||||||
p.ordered = append(p.ordered, key)
|
p.ordered = append(p.ordered, key)
|
||||||
case itemKeyStart:
|
case itemKeyStart:
|
||||||
kname := p.expect(itemText)
|
kname := p.next()
|
||||||
p.currentKey = kname.val
|
|
||||||
p.approxLine = kname.line
|
p.approxLine = kname.line
|
||||||
|
p.currentKey = p.keyString(kname)
|
||||||
|
|
||||||
val, typ := p.value(p.next())
|
val, typ := p.value(p.next())
|
||||||
p.setValue(p.currentKey, val)
|
p.setValue(p.currentKey, val)
|
||||||
p.setType(p.currentKey, typ)
|
p.setType(p.currentKey, typ)
|
||||||
p.ordered = append(p.ordered, p.context.add(p.currentKey))
|
p.ordered = append(p.ordered, p.context.add(p.currentKey))
|
||||||
|
|
||||||
p.currentKey = ""
|
p.currentKey = ""
|
||||||
default:
|
default:
|
||||||
p.bug("Unexpected type at top level: %s", item.typ)
|
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
|
// value translates an expected value from the lexer into a Go value wrapped
|
||||||
// as an empty interface.
|
// as an empty interface.
|
||||||
func (p *parser) value(it item) (interface{}, tomlType) {
|
func (p *parser) value(it item) (interface{}, tomlType) {
|
||||||
switch it.typ {
|
switch it.typ {
|
||||||
case itemString:
|
case itemString:
|
||||||
return p.replaceUnicode(replaceEscapes(it.val)), p.typeOfPrimitive(it)
|
return p.replaceEscapes(it.val), p.typeOfPrimitive(it)
|
||||||
case itemMultilineString:
|
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:
|
case itemRawString:
|
||||||
return it.val, p.typeOfPrimitive(it)
|
return it.val, p.typeOfPrimitive(it)
|
||||||
case itemRawMultilineString:
|
case itemRawMultilineString:
|
||||||
@@ -164,10 +178,18 @@ func (p *parser) value(it item) (interface{}, tomlType) {
|
|||||||
}
|
}
|
||||||
p.bug("Expected boolean value, but got '%s'.", it.val)
|
p.bug("Expected boolean value, but got '%s'.", it.val)
|
||||||
case itemInteger:
|
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 {
|
if err != nil {
|
||||||
// See comment below for floats describing why we make a
|
// Distinguish integer values. Normally, it'd be a bug if the lexer
|
||||||
// distinction between a bug and a user error.
|
// 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 &&
|
if e, ok := err.(*strconv.NumError); ok &&
|
||||||
e.Err == strconv.ErrRange {
|
e.Err == strconv.ErrRange {
|
||||||
|
|
||||||
@@ -179,29 +201,57 @@ func (p *parser) value(it item) (interface{}, tomlType) {
|
|||||||
}
|
}
|
||||||
return num, p.typeOfPrimitive(it)
|
return num, p.typeOfPrimitive(it)
|
||||||
case itemFloat:
|
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 {
|
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 &&
|
if e, ok := err.(*strconv.NumError); ok &&
|
||||||
e.Err == strconv.ErrRange {
|
e.Err == strconv.ErrRange {
|
||||||
|
|
||||||
p.panicf("Float '%s' is out of the range of 64-bit "+
|
p.panicf("Float '%s' is out of the range of 64-bit "+
|
||||||
"IEEE-754 floating-point numbers.", it.val)
|
"IEEE-754 floating-point numbers.", it.val)
|
||||||
} else {
|
} else {
|
||||||
p.bug("Expected float value, but got '%s'.", it.val)
|
p.panicf("Invalid float value: %q", it.val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return num, p.typeOfPrimitive(it)
|
return num, p.typeOfPrimitive(it)
|
||||||
case itemDatetime:
|
case itemDatetime:
|
||||||
t, err := time.Parse("2006-01-02T15:04:05Z", it.val)
|
var t time.Time
|
||||||
if err != nil {
|
var ok bool
|
||||||
p.bug("Expected Zulu formatted DateTime, but got '%s'.", it.val)
|
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)
|
return t, p.typeOfPrimitive(it)
|
||||||
case itemArray:
|
case itemArray:
|
||||||
@@ -219,11 +269,75 @@ func (p *parser) value(it item) (interface{}, tomlType) {
|
|||||||
types = append(types, typ)
|
types = append(types, typ)
|
||||||
}
|
}
|
||||||
return array, p.typeOfArray(types)
|
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)
|
p.bug("Unexpected value type: %s", it.typ)
|
||||||
panic("unreachable")
|
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,
|
// establishContext sets the current context of the parser,
|
||||||
// where the context is either a hash or an array of hashes. Which one is
|
// where the context is either a hash or an array of hashes. Which one is
|
||||||
// set depends on the value of the `array` parameter.
|
// set depends on the value of the `array` parameter.
|
||||||
@@ -359,7 +473,8 @@ func (p *parser) addImplicit(key Key) {
|
|||||||
p.implicits[key.String()] = true
|
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) {
|
func (p *parser) removeImplicit(key Key) {
|
||||||
p.implicits[key.String()] = false
|
p.implicits[key.String()] = false
|
||||||
}
|
}
|
||||||
@@ -381,64 +496,97 @@ func (p *parser) current() string {
|
|||||||
return fmt.Sprintf("%s.%s", p.context, p.currentKey)
|
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 {
|
func stripFirstNewline(s string) string {
|
||||||
if len(s) == 0 || s[0] != '\n' {
|
if len(s) == 0 || s[0] != '\n' {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
return s[1:]
|
||||||
return s[1:len(s)]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func stripEscapedWhitespace(s string) string {
|
func stripEscapedWhitespace(s string) string {
|
||||||
esc := strings.Split(s, "\\\n")
|
esc := strings.Split(s, "\\\n")
|
||||||
|
|
||||||
if len(esc) > 1 {
|
if len(esc) > 1 {
|
||||||
for i := 1; i < len(esc); i++ {
|
for i := 1; i < len(esc); i++ {
|
||||||
esc[i] = strings.TrimLeftFunc(esc[i], unicode.IsSpace)
|
esc[i] = strings.TrimLeftFunc(esc[i], unicode.IsSpace)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.Join(esc, "")
|
return strings.Join(esc, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) replaceUnicode(s string) string {
|
func (p *parser) replaceEscapes(str string) string {
|
||||||
indexEsc := func() int {
|
var replaced []rune
|
||||||
return strings.Index(s, "\\u")
|
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() {
|
return string(replaced)
|
||||||
asciiBytes := s[i+2 : i+6]
|
|
||||||
s = strings.Replace(s, s[i:i+6], p.asciiEscapeToUnicode(asciiBytes), -1)
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
hex, err := strconv.ParseUint(strings.ToLower(s), 16, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.bug("Could not parse '%s' as a hexadecimal number, but the "+
|
p.bug("Could not parse '%s' as a hexadecimal number, but the "+
|
||||||
"lexer claims it's OK: %s", s, err)
|
"lexer claims it's OK: %s", s, err)
|
||||||
}
|
}
|
||||||
|
if !utf8.ValidRune(rune(hex)) {
|
||||||
// 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) {
|
|
||||||
p.panicf("Escaped character '\\u%s' is not valid UTF-8.", s)
|
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]
|
theType := types[0]
|
||||||
for _, t := range types[1:] {
|
for _, t := range types[1:] {
|
||||||
if !typeEqual(theType, t) {
|
if !typeEqual(theType, t) {
|
||||||
p.panicf("Array contains values of type '%s' and '%s', but arrays "+
|
p.panicf("Array contains values of type '%s' and '%s', but "+
|
||||||
"must be homogeneous.", theType, t)
|
"arrays must be homogeneous.", theType, t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return tomlArray
|
return tomlArray
|
||||||
|
|||||||
+6
-5
@@ -92,11 +92,11 @@ func typeFields(t reflect.Type) []field {
|
|||||||
// Scan f.typ for fields to include.
|
// Scan f.typ for fields to include.
|
||||||
for i := 0; i < f.typ.NumField(); i++ {
|
for i := 0; i < f.typ.NumField(); i++ {
|
||||||
sf := f.typ.Field(i)
|
sf := f.typ.Field(i)
|
||||||
if sf.PkgPath != "" { // unexported
|
if sf.PkgPath != "" && !sf.Anonymous { // unexported
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name := sf.Tag.Get("toml")
|
opts := getOptions(sf.Tag)
|
||||||
if name == "-" {
|
if opts.skip {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
index := make([]int, len(f.index)+1)
|
index := make([]int, len(f.index)+1)
|
||||||
@@ -110,8 +110,9 @@ func typeFields(t reflect.Type) []field {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Record found field and index sequence.
|
// Record found field and index sequence.
|
||||||
if name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct {
|
if opts.name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct {
|
||||||
tagged := name != ""
|
tagged := opts.name != ""
|
||||||
|
name := opts.name
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = sf.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
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
copyright notice and this permission notice appear in all copies.
|
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
|
// Permission to use, copy, modify, and distribute this software for any
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
// 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.
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
// NOTE: Due to the following build constraints, this file will only be compiled
|
// 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"
|
// when the code is not running on Google App Engine, compiled by GopherJS, and
|
||||||
// is not added to the go build command line.
|
// "-tags safe" is not added to the go build command line. The "disableunsafe"
|
||||||
// +build !appengine,!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
|
package spew
|
||||||
|
|
||||||
@@ -33,80 +36,49 @@ const (
|
|||||||
ptrSize = unsafe.Sizeof((*byte)(nil))
|
ptrSize = unsafe.Sizeof((*byte)(nil))
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
type flag uintptr
|
||||||
// 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)
|
|
||||||
|
|
||||||
// flagKindWidth and flagKindShift indicate various bits that the
|
var (
|
||||||
// reflect package uses internally to track kind information.
|
// flagRO indicates whether the value field of a reflect.Value
|
||||||
//
|
// is read-only.
|
||||||
// flagRO indicates whether or not the value field of a reflect.Value is
|
flagRO flag
|
||||||
// read-only.
|
|
||||||
//
|
// flagAddr indicates whether the address of the reflect.Value's
|
||||||
// flagIndir indicates whether the value field of a reflect.Value is
|
// value may be taken.
|
||||||
// the actual data or a pointer to the data.
|
flagAddr flag
|
||||||
//
|
|
||||||
// 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)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
// flagKindMask holds the bits that make up the kind
|
||||||
// Older versions of reflect.Value stored small integers directly in the
|
// part of the flags field. In all the supported versions,
|
||||||
// ptr field (which is named val in the older versions). Versions
|
// it is in the lower 5 bits.
|
||||||
// between commits ecccf07e7f9d and 82f48826c6c7 added a new field named
|
const flagKindMask = flag(0x1f)
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit 90a7c3c86944 changed the flag positions such that the low
|
// Different versions of Go have used different
|
||||||
// order bits are the kind. This code extracts the kind from the flags
|
// bit layouts for the flags type. This table
|
||||||
// field and ensures it's the correct type. When it's not, the flag
|
// records the known combinations.
|
||||||
// order has been changed to the newer format, so the flags are updated
|
var okFlags = []struct {
|
||||||
// accordingly.
|
ro, addr flag
|
||||||
upf := unsafe.Pointer(uintptr(unsafe.Pointer(&vv)) + offsetFlag)
|
}{{
|
||||||
upfv := *(*uintptr)(upf)
|
// From Go 1.4 to 1.5
|
||||||
flagKindMask := uintptr((1<<flagKindWidth - 1) << flagKindShift)
|
ro: 1 << 5,
|
||||||
if (upfv&flagKindMask)>>flagKindShift != uintptr(reflect.Int) {
|
addr: 1 << 7,
|
||||||
flagKindShift = 0
|
}, {
|
||||||
flagRO = 1 << 5
|
// Up to Go tip.
|
||||||
flagIndir = 1 << 6
|
ro: 1<<5 | 1<<6,
|
||||||
|
addr: 1 << 8,
|
||||||
|
}}
|
||||||
|
|
||||||
// Commit adf9b30e5594 modified the flags to separate the
|
var flagValOffset = func() uintptr {
|
||||||
// flagRO flag into two bits which specifies whether or not the
|
field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
|
||||||
// field is embedded. This causes flagIndir to move over a bit
|
if !ok {
|
||||||
// and means that flagRO is the combination of either of the
|
panic("reflect.Value has no flag field")
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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
|
// 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
|
// This allows us to check for implementations of the Stringer and error
|
||||||
// interfaces to be used for pretty printing ordinarily unaddressable and
|
// interfaces to be used for pretty printing ordinarily unaddressable and
|
||||||
// inaccessible values such as unexported struct fields.
|
// inaccessible values such as unexported struct fields.
|
||||||
func unsafeReflectValue(v reflect.Value) (rv reflect.Value) {
|
func unsafeReflectValue(v reflect.Value) reflect.Value {
|
||||||
indirects := 1
|
if !v.IsValid() || (v.CanInterface() && v.CanAddr()) {
|
||||||
vt := v.Type()
|
return v
|
||||||
upv := unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetPtr)
|
}
|
||||||
rvf := *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetFlag))
|
flagFieldPtr := flagField(&v)
|
||||||
if rvf&flagIndir != 0 {
|
*flagFieldPtr &^= flagRO
|
||||||
vt = reflect.PtrTo(v.Type())
|
*flagFieldPtr |= flagAddr
|
||||||
indirects++
|
return v
|
||||||
} else if offsetScalar != 0 {
|
}
|
||||||
// The value is in the scalar field when it's not one of the
|
|
||||||
// reference types.
|
// Sanity checks against future reflect package changes
|
||||||
switch vt.Kind() {
|
// to the type or semantics of the Value.flag field.
|
||||||
case reflect.Uintptr:
|
func init() {
|
||||||
case reflect.Chan:
|
field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
|
||||||
case reflect.Func:
|
if !ok {
|
||||||
case reflect.Map:
|
panic("reflect.Value has no flag field")
|
||||||
case reflect.Ptr:
|
}
|
||||||
case reflect.UnsafePointer:
|
if field.Type.Kind() != reflect.TypeOf(flag(0)).Kind() {
|
||||||
default:
|
panic("reflect.Value flag field has changed kind")
|
||||||
upv = unsafe.Pointer(uintptr(unsafe.Pointer(&v)) +
|
}
|
||||||
offsetScalar)
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
panic("reflect.Value read-only flag has changed semantics")
|
||||||
pv := reflect.NewAt(vt, upv)
|
|
||||||
rv = pv
|
|
||||||
for i := 0; i < indirects; i++ {
|
|
||||||
rv = rv.Elem()
|
|
||||||
}
|
|
||||||
return rv
|
|
||||||
}
|
}
|
||||||
|
|||||||
+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
|
// Permission to use, copy, modify, and distribute this software for any
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
// 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.
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
// NOTE: Due to the following build constraints, this file will only be compiled
|
// 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"
|
// when the code is running on Google App Engine, compiled by GopherJS, or
|
||||||
// is added to the go build command line.
|
// "-tags safe" is added to the go build command line. The "disableunsafe"
|
||||||
// +build appengine disableunsafe
|
// tag is deprecated and thus should not be used.
|
||||||
|
// +build js appengine safe disableunsafe !go1.4
|
||||||
|
|
||||||
package spew
|
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
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
* purpose with or without fee is hereby granted, provided that the above
|
* 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)
|
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.
|
// prefix to Writer w.
|
||||||
func printHexPtr(w io.Writer, p uintptr) {
|
func printHexPtr(w io.Writer, p uintptr) {
|
||||||
// Null pointer.
|
// 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
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
* purpose with or without fee is hereby granted, provided that the above
|
* 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
|
// inside these interface methods. As a result, this option relies on
|
||||||
// access to the unsafe package, so it will not have any effect when
|
// access to the unsafe package, so it will not have any effect when
|
||||||
// running in environments without access to the unsafe package such as
|
// 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
|
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
|
// ContinueOnMethod specifies whether or not recursion should continue once
|
||||||
// a custom error or Stringer interface is invoked. The default, false,
|
// a custom error or Stringer interface is invoked. The default, false,
|
||||||
// means it will print the results of invoking the custom error or Stringer
|
// 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
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
* purpose with or without fee is hereby granted, provided that the above
|
* 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.
|
which only accept pointer receivers from non-pointer variables.
|
||||||
Pointer method invocation is enabled by default.
|
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
|
* ContinueOnMethod
|
||||||
Enables recursion into types after invoking error and Stringer interface
|
Enables recursion into types after invoking error and Stringer interface
|
||||||
methods. Recursion after method invocation is disabled by default.
|
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
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
* purpose with or without fee is hereby granted, provided that the above
|
* 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.
|
// cCharRE is a regular expression that matches a cgo char.
|
||||||
// It is used to detect character arrays to hexdump them.
|
// 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
|
// cUnsignedCharRE is a regular expression that matches a cgo unsigned
|
||||||
// char. It is used to detect unsigned character arrays to hexdump
|
// char. It is used to detect unsigned character arrays to hexdump
|
||||||
// them.
|
// them.
|
||||||
cUnsignedCharRE = regexp.MustCompile("^.*\\._Ctype_unsignedchar$")
|
cUnsignedCharRE = regexp.MustCompile(`^.*\._Ctype_unsignedchar$`)
|
||||||
|
|
||||||
// cUint8tCharRE is a regular expression that matches a cgo uint8_t.
|
// cUint8tCharRE is a regular expression that matches a cgo uint8_t.
|
||||||
// It is used to detect uint8_t arrays to hexdump them.
|
// 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.
|
// 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)
|
d.w.Write(closeParenBytes)
|
||||||
|
|
||||||
// Display pointer information.
|
// Display pointer information.
|
||||||
if len(pointerChain) > 0 {
|
if !d.cs.DisablePointerAddresses && len(pointerChain) > 0 {
|
||||||
d.w.Write(openParenBytes)
|
d.w.Write(openParenBytes)
|
||||||
for i, addr := range pointerChain {
|
for i, addr := range pointerChain {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
@@ -143,10 +143,10 @@ func (d *dumpState) dumpPtr(v reflect.Value) {
|
|||||||
// Display dereferenced value.
|
// Display dereferenced value.
|
||||||
d.w.Write(openParenBytes)
|
d.w.Write(openParenBytes)
|
||||||
switch {
|
switch {
|
||||||
case nilFound == true:
|
case nilFound:
|
||||||
d.w.Write(nilAngleBytes)
|
d.w.Write(nilAngleBytes)
|
||||||
|
|
||||||
case cycleFound == true:
|
case cycleFound:
|
||||||
d.w.Write(circularBytes)
|
d.w.Write(circularBytes)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -282,13 +282,13 @@ func (d *dumpState) dump(v reflect.Value) {
|
|||||||
case reflect.Map, reflect.String:
|
case reflect.Map, reflect.String:
|
||||||
valueLen = v.Len()
|
valueLen = v.Len()
|
||||||
}
|
}
|
||||||
if valueLen != 0 || valueCap != 0 {
|
if valueLen != 0 || !d.cs.DisableCapacities && valueCap != 0 {
|
||||||
d.w.Write(openParenBytes)
|
d.w.Write(openParenBytes)
|
||||||
if valueLen != 0 {
|
if valueLen != 0 {
|
||||||
d.w.Write(lenEqualsBytes)
|
d.w.Write(lenEqualsBytes)
|
||||||
printInt(d.w, int64(valueLen), 10)
|
printInt(d.w, int64(valueLen), 10)
|
||||||
}
|
}
|
||||||
if valueCap != 0 {
|
if !d.cs.DisableCapacities && valueCap != 0 {
|
||||||
if valueLen != 0 {
|
if valueLen != 0 {
|
||||||
d.w.Write(spaceBytes)
|
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
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
* purpose with or without fee is hereby granted, provided that the above
|
* 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.
|
// Display dereferenced value.
|
||||||
switch {
|
switch {
|
||||||
case nilFound == true:
|
case nilFound:
|
||||||
f.fs.Write(nilAngleBytes)
|
f.fs.Write(nilAngleBytes)
|
||||||
|
|
||||||
case cycleFound == true:
|
case cycleFound:
|
||||||
f.fs.Write(circularShortBytes)
|
f.fs.Write(circularShortBytes)
|
||||||
|
|
||||||
default:
|
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
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
* purpose with or without fee is hereby granted, provided that the above
|
* 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/*
|
vendor/*
|
||||||
!Godeps/Godeps.json
|
!vendor/vendor.json
|
||||||
|
coverage.out
|
||||||
|
count.out
|
||||||
|
test
|
||||||
|
|||||||
+32
-3
@@ -1,6 +1,35 @@
|
|||||||
language: go
|
language: go
|
||||||
sudo: false
|
sudo: false
|
||||||
go:
|
go:
|
||||||
- 1.3
|
- 1.6.x
|
||||||
- 1.4
|
- 1.7.x
|
||||||
- tip
|
- 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.
|
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)
|
**Maintainers:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho)
|
||||||
**Long-term Maintainer:** Javier Provecho (@javierprovecho)
|
|
||||||
|
|
||||||
People and companies, who have contributed, in alphabetical order.
|
People and companies, who have contributed, in alphabetical order.
|
||||||
|
|
||||||
|
**@858806258 (杰哥)**
|
||||||
|
- Fix typo in example
|
||||||
|
|
||||||
|
|
||||||
**@achedeuzot (Klemen Sever)**
|
**@achedeuzot (Klemen Sever)**
|
||||||
- Fix newline debug printing
|
- Fix newline debug printing
|
||||||
|
|
||||||
@@ -21,6 +26,10 @@ People and companies, who have contributed, in alphabetical order.
|
|||||||
- Typos in README
|
- Typos in README
|
||||||
|
|
||||||
|
|
||||||
|
**@alexanderdidenko (Aleksandr Didenko)**
|
||||||
|
- Add support multipart/form-data
|
||||||
|
|
||||||
|
|
||||||
**@alexandernyquist (Alexander Nyquist)**
|
**@alexandernyquist (Alexander Nyquist)**
|
||||||
- Using template.Must to fix multiple return issue
|
- Using template.Must to fix multiple return issue
|
||||||
- ★ Added support for OPTIONS verb
|
- ★ Added support for OPTIONS verb
|
||||||
@@ -55,15 +64,39 @@ People and companies, who have contributed, in alphabetical order.
|
|||||||
- Add example about serving static files
|
- Add example about serving static files
|
||||||
|
|
||||||
|
|
||||||
|
**@donileo (Adonis)**
|
||||||
|
- Add NoMethod handler
|
||||||
|
|
||||||
|
|
||||||
**@dutchcoders (DutchCoders)**
|
**@dutchcoders (DutchCoders)**
|
||||||
- ★ Fix security bug that allows client to spoof ip
|
- ★ Fix security bug that allows client to spoof ip
|
||||||
- Fix typo. r.HTMLTemplates -> SetHTMLTemplate
|
- 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)**
|
**@fmd (Fareed Dudhia)**
|
||||||
- Fix typo. SetHTTPTemplate -> SetHTMLTemplate
|
- Fix typo. SetHTTPTemplate -> SetHTMLTemplate
|
||||||
|
|
||||||
|
|
||||||
|
**@ironiridis (Christopher Harrington)**
|
||||||
|
- Remove old reference
|
||||||
|
|
||||||
|
|
||||||
**@jammie-stackhouse (Jamie Stackhouse)**
|
**@jammie-stackhouse (Jamie Stackhouse)**
|
||||||
- Add more shortcuts for router methods
|
- 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
|
- ★ work around path.Join removing trailing slashes from routes
|
||||||
|
|
||||||
|
|
||||||
|
**@mattn (Yasuhiro Matsumoto)**
|
||||||
|
- Improve color logger
|
||||||
|
|
||||||
|
|
||||||
**@mdigger (Dmitry Sedykh)**
|
**@mdigger (Dmitry Sedykh)**
|
||||||
- Fixes Form binding when content-type is x-www-form-urlencoded
|
- Fixes Form binding when content-type is x-www-form-urlencoded
|
||||||
- No repeat call c.Writer.Status() in gin.Logger
|
- 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.
|
- Fix Port usage in README.
|
||||||
|
|
||||||
|
|
||||||
|
**@rayrod2030 (Ray Rodriguez)**
|
||||||
|
- Fix typo in example
|
||||||
|
|
||||||
|
|
||||||
|
**@rns**
|
||||||
|
- Fix typo in example
|
||||||
|
|
||||||
|
|
||||||
**@RobAWilkinson (Robert Wilkinson)**
|
**@RobAWilkinson (Robert Wilkinson)**
|
||||||
- Add example of forms and params
|
- Add example of forms and params
|
||||||
|
|
||||||
|
|
||||||
|
**@rogierlommers (Rogier Lommers)**
|
||||||
|
- Add updated static serve example
|
||||||
|
|
||||||
|
|
||||||
**@se77en (Damon Zhao)**
|
**@se77en (Damon Zhao)**
|
||||||
- Improve color logging
|
- Improve color logging
|
||||||
|
|
||||||
@@ -166,9 +215,17 @@ People and companies, who have contributed, in alphabetical order.
|
|||||||
- Update httprouter godeps
|
- Update httprouter godeps
|
||||||
|
|
||||||
|
|
||||||
|
**@tebeka (Miki Tebeka)**
|
||||||
|
- Use net/http constants instead of numeric values
|
||||||
|
|
||||||
|
|
||||||
|
**@techjanitor**
|
||||||
|
- Update context.go reserved IPs
|
||||||
|
|
||||||
|
|
||||||
**@yosssi (Keiji Yoshida)**
|
**@yosssi (Keiji Yoshida)**
|
||||||
- Fix link in README
|
- Fix link in README
|
||||||
|
|
||||||
|
|
||||||
**@yuyabee**
|
**@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