Compare commits

...
379 Commits
Author SHA1 Message Date
Dan Sosedoff d3aaf8f5db Release 0.11.2 2019-02-15 14:09:49 -06:00
Dan SosedoffandGitHub 26cd70cbaa Merge pull request #414 from sosedoff/fix-estimation-for-camelcase-schemas
Fix estimation for camelcase schemas
2019-02-08 14:25:19 -06:00
Dan Sosedoff 65aae5ec95 Add extra test cases for camelCase table names 2019-02-08 14:02:36 -06:00
Dan Sosedoff 5af4332593 Fix SQL query for camelCase tables row estimation 2019-02-08 13:27:40 -06:00
Dan Sosedoff 12717f15cc Changelog update 2019-01-28 17:18:55 -06:00
Dan Sosedoff dc1009cdd8 Release 0.11.1 2019-01-28 17:11:04 -06:00
Dan SosedoffandGitHub 52bda5f823 Merge pull request #409 from sosedoff/switch-bindata-dep
Switch to github.com/go-bindata/go-bindata fork
2019-01-28 16:51:43 -06:00
Dan Sosedoff 44b6932202 Regenerate bindata 2019-01-28 16:45:41 -06:00
Dan Sosedoff 0556d8b69d Switch to github.com/go-bindata/go-bindata fork 2019-01-28 16:32:43 -06:00
Dan SosedoffandGitHub 01516e030b Merge pull request #408 from sosedoff/fix-auto-user-detection
Fix error when automatic user detection fails
2019-01-28 14:30:57 -06:00
Dan Sosedoff c32d68809b Add extra note 2019-01-28 14:22:05 -06:00
Dan Sosedoff d48172986e Fix error when automatic user detection fails 2019-01-28 14:03:45 -06:00
Dan Sosedoff e6f6dbbddd Update license years 2019-01-28 12:08:25 -06:00
Dan Sosedoff 7995a588bd Remove apt cache when building docker container 2019-01-28 12:02:30 -06:00
Dan SosedoffandGitHub baa587166c Merge pull request #405 from Nhat002/fix_encode_query_to_base64_in_export
Add custome base64 encode to handle special characters on query to export result
2019-01-28 11:47:36 -06:00
Dan SosedoffandGitHub 8dab66567d Merge pull request #406 from sosedoff/fix-startup-error-with-db-url
Fix startup error when DATABASE_URL is set
2019-01-25 11:08:02 -06:00
Dan Sosedoff a7959ad0fa Fix startup error when DATABASE_URL is set 2019-01-25 10:29:24 -06:00
Nhat Tran 25304f8229 Fix: missing base64.js in index.html and remove blank space 2019-01-25 12:04:21 +08:00
Nhat Tran bc0602c523 Add custome base64 encode to handle special character 2019-01-24 15:35:16 +08:00
Dan Sosedoff ae60604ef1 Fix typo 2019-01-16 22:54:23 -06:00
Dan Sosedoff 4bdcfbd5b1 Update changelog/dockerfile 2018-12-24 13:08:20 -06:00
Dan Sosedoff 53c02d8914 Release 0.11.0 2018-12-24 12:57:39 -06:00
Dan SosedoffandGitHub 4d74052ad8 Merge pull request #401 from sosedoff/db-object-counter-style
Tweak sidebar database object counters styles
2018-12-19 19:41:45 -06:00
Dan Sosedoff 435a67cb5d Tweak sidebar database object counters styles 2018-12-19 12:40:28 -06:00
Dan SosedoffandGitHub aadee6ac8a Merge pull request #399 from sosedoff/fix-start-with-default-db
Do not exit with error if local server is not running
2018-12-18 20:23:09 -06:00
Dan Sosedoff 9c07ac356b Do not exit with error if local server is not running 2018-12-18 18:11:56 -06:00
Dan SosedoffandGitHub 9a3c79d93d Merge pull request #398 from sosedoff/ssh-host-callback-fix
SSH connection fix
2018-12-14 20:18:41 -06:00
Dan Sosedoff 2bd9f39da5 Fix SSH error: ssh: must specify HostKeyCallback 2018-12-14 19:56:33 -06:00
Dan Sosedoff c4e63dcc85 Regenerate bindata 2018-12-13 22:36:38 -06:00
Dan Sosedoff 794e5a22c1 Rename command.VERSION to command.Version 2018-12-13 22:35:43 -06:00
Dan Sosedoff eeb5245cb9 Typo fix 2018-12-13 17:56:57 -06:00
Dan SosedoffandGitHub e8328b25bc Merge pull request #397 from sosedoff/restrict-activity
Scope activity list to current database
2018-12-13 13:12:49 -06:00
Dan Sosedoff 9c1ce91de1 Scope activity list to current database 2018-12-13 12:14:10 -06:00
Dan Sosedoff 073bcea56f Add note about pgweb pro version 2018-12-12 21:08:34 -06:00
Dan SosedoffandGitHub 9e3872c980 Merge pull request #396 from sosedoff/show-pgweb-version
Show current release and check for updates
2018-12-12 20:52:20 -06:00
Dan Sosedoff 658d14314b Show pgweb version on homep age and check for newer releases 2018-12-12 20:35:07 -06:00
Dan SosedoffandGitHub 107a3ac6d4 Merge pull request #395 from sosedoff/fix-bookmark-switch
Force switch back to default connection settings view
2018-12-12 20:19:37 -06:00
Dan Sosedoff ad82841eb2 Force switch back to default connection settings view 2018-12-12 18:29:07 -06:00
Dan SosedoffandGitHub c9d94b54d7 Merge pull request #394 from sosedoff/fix-row-estimation
Fix row estimation bug
2018-12-12 14:33:06 -06:00
Dan SosedoffandGitHub d96ddf9b18 Merge pull request #393 from sosedoff/debug-failed-queries
Print out failed query SQL and args with --debug flag
2018-12-12 13:57:17 -06:00
Dan Sosedoff 7917c2ad35 Simplify TableRowsCount func logic 2018-12-12 13:56:08 -06:00
Dan Sosedoff 2db5ce544f Fix row estimation bug
The variable `table` is mutated from `schema.tablename` to `tablename`
before being passed to other functions, which try to parse the schema
from the name. This results in schema name being `public` because it's
missing from the given table name.
2018-12-12 13:33:55 -06:00
Dan Sosedoff 474b99c6d5 Print out failed query SQL and args with --debug flag 2018-12-12 13:22:36 -06:00
Dan SosedoffandGitHub aded63f76f Merge pull request #390 from sosedoff/refactor2
Cleanup and refactor
2018-12-04 12:54:17 -06:00
Dan Sosedoff d1a7e6ecfe More lint 2018-12-04 12:42:37 -06:00
Dan Sosedoff a5b8e02a98 Cleanup backend handler 2018-12-01 22:35:11 -06:00
Dan Sosedoff 72af00f1b7 Refactor api package 2018-11-30 21:40:28 -06:00
Dan Sosedoff b86a849e00 Update pgweb version in dockerfile to 0.10.0 2018-11-28 10:39:02 -06:00
Dan Sosedoff 09c3a1862a Update changelog 2018-11-28 10:38:42 -06:00
Dan Sosedoff 54da27ef70 Release 0.10.0 2018-11-28 10:25:37 -06:00
Dan Sosedoff d9728cbb7a Changelog 2018-11-27 17:50:47 -06:00
Dan SosedoffandGitHub 420478ce6e Merge pull request #389 from sosedoff/fix-startup-with-default-db
Fix startup behavior when user did not provide a database name
2018-11-27 17:32:16 -06:00
Dan Sosedoff 21d2893292 Fix startup behavior when user did not provide a database name 2018-11-27 17:23:55 -06:00
Dan SosedoffandGitHub 1189bdef33 Merge pull request #388 from sosedoff/fix-json-naan-panic
Fix JSON marshal panic when dealing with NaN values
2018-11-27 17:00:38 -06:00
Dan SosedoffandGitHub 15d2e7917a Merge pull request #387 from sosedoff/url-prefix-env-var
Allow settings url prefix with URL_PREFIX env var
2018-11-27 17:00:10 -06:00
Dan Sosedoff 903a265676 Fix JSON marshal panic when dealing with NaN values 2018-11-27 16:51:51 -06:00
Dan Sosedoff 965cf754cf Allow settings url prefix with URL_PREFIX env var 2018-11-27 14:58:50 -06:00
Dan Sosedoff 3850eedf07 Add docker badge 2018-11-19 18:18:29 -06:00
Dan SosedoffandGitHub 0b400b1ffd Merge pull request #386 from HaraldNordgren/go_versions
Bump Go versions and use '.x' to always get latest patch versions
2018-11-14 18:40:21 -06:00
Dan Sosedoff abb143601b Add test for serverResult func 2018-11-05 17:07:32 -06:00
Harald Nordgren 4d193f0dd6 Bump Go versions and use '.x' to always get latest patch versions 2018-10-28 13:33:16 +01:00
Dan SosedoffandGitHub 9ca959c31c Merge pull request #381 from sosedoff/row-context-menu
Table row context menu
2018-09-16 16:42:51 -05:00
Dan Sosedoff 29d4b276e2 Do not show row context menu if results are empty 2018-09-14 18:19:24 -05:00
Dan Sosedoff 6ed3d5b501 Add result row context menu to copy value or filter rows 2018-09-14 18:14:18 -05:00
Dan Sosedoff 14037ddad2 Take out failing test due to new sessions behavior 2018-09-14 01:00:47 -05:00
Dan Sosedoff 1ecf18e41c Also flush bookmark under sessions/connect setup 2018-09-14 00:56:16 -05:00
Dan Sosedoff badcf700b9 Clear out connection settings when running in session/connect mode 2018-09-14 00:54:42 -05:00
Dan SosedoffandGitHub 527c680e1f Merge pull request #380 from sosedoff/localhost-default
Make localhost to be a default db host
2018-09-14 00:14:35 -05:00
Dan Sosedoff 174b2ffdbb Make localhost to be a default db host 2018-09-14 00:04:02 -05:00
Dan SosedoffandGitHub b4e2f2f13e Merge pull request #379 from sosedoff/numeric-stats-count
Include rows count to numeric stats view on table column
2018-09-14 00:00:11 -05:00
Dan Sosedoff fb7676ab4e Include rows count to numeric stats view on table column 2018-09-13 23:46:33 -05:00
Dan Sosedoff c9018e843e Regenerate bindata 2018-09-13 23:41:59 -05:00
Dan SosedoffandGitHub 31db9658e0 Merge pull request #377 from sosedoff/column-numstats
Add column context menu item to get numeric stats
2018-09-13 23:32:43 -05:00
Dan SosedoffandGitHub 449254bcfd Merge pull request #378 from sosedoff/connection-string-fixes
Fix issues with connection string builder
2018-09-13 23:31:06 -05:00
Dan Sosedoff fc380df8dd Fix failing win test 2018-09-13 23:22:25 -05:00
Dan Sosedoff dc4e8598f7 Refactor building connection string from options 2018-09-13 22:44:11 -05:00
Dan Sosedoff b0151ee985 Refactor connection string generator 2018-09-13 22:25:15 -05:00
Dan Sosedoff b9eba7bfa4 Add column context menu item to get numeric stats 2018-09-13 20:50:33 -05:00
Dan Sosedoff cad6c39e72 Update root package 2018-09-05 10:43:56 -05:00
Dan Sosedoff 2e64b56d14 Specify metadata for heroku 2018-09-05 10:35:45 -05:00
Dan SosedoffandGitHub 1620f2be56 Merge pull request #375 from sosedoff/dep
Dependencies update and switch to dep
2018-09-04 09:02:09 -05:00
Dan Sosedoff f6fa44b7fc Update go test script 2018-08-31 22:37:24 -05:00
Dan Sosedoff ab33472bf4 Turn off CGO for win 2018-08-31 22:30:59 -05:00
Dan Sosedoff 5ad15326ec Another appveyor pass 2018-08-31 22:24:36 -05:00
Dan Sosedoff 86c301bcf9 Try appveyor without overriding GOPATH 2018-08-31 22:19:48 -05:00
Dan Sosedoff 7a52df61ec Add missing dep install 2018-08-31 22:13:01 -05:00
Dan Sosedoff 0c40f1907e Update appveyor build script 2018-08-31 22:11:54 -05:00
Dan Sosedoff 35641e5957 Remove failing mime types from test 2018-08-31 22:01:51 -05:00
Dan Sosedoff 82130ecce0 Update CI matrix to use latest 2 versions of go 2018-08-31 21:57:48 -05:00
Dan Sosedoff ad81f666a5 Switch to dep for dependency management 2018-08-31 21:49:24 -05:00
Dan Sosedoff 847f47c5d3 Remove premium/donations section from readme 2018-08-20 21:18:49 -05:00
Dan Sosedoff aefcf81bb3 Changelog update 2018-08-20 21:15:50 -05:00
Dan Sosedoff e3b2d08021 Update dockerfile 2018-08-20 21:04:10 -05:00
Dan SosedoffandGitHub 3da2af9111 Merge pull request #370 from sosedoff/table-filter-tweaks
Automatically set table filter option to 'equals' if its not set
2018-08-20 20:54:16 -05:00
Dan Sosedoff a6eade1ae1 Automatically set table filter option to 'equals' if its not set 2018-08-20 20:42:38 -05:00
Dan SosedoffandGitHub 3b3d739e91 Merge pull request #356 from ericdagenais/itn/table-quotes
Fixes relation not found errors when dealing with table names that have uppercase characters
2018-06-26 22:23:48 -05:00
Eric Dagenais 9bb3afe6ce Merge remote-tracking branch 'upstream/master' into itn/tables-quotes-2
# Conflicts:
#	pkg/client/client.go
2018-06-20 07:19:34 -07:00
Dan SosedoffandGitHub 41a99be4ca Merge pull request #366 from allisson/master
Add EstimatedTableRowsCount to avoid count in large tables
2018-06-18 10:26:56 -05:00
Allisson Azevedo 808f6eea33 Change EstimatedTableRowsCount query 2018-06-16 18:12:11 -03:00
Allisson Azevedo 974570c79e Add more tests 2018-06-16 11:09:37 -03:00
Allisson Azevedo 5ba53d3cbd Add test_EstimatedTableRowsCount 2018-06-14 17:51:16 -03:00
Allisson Azevedo 8748f07167 Use EstimatedTableRowsCount only for postgresql 2018-06-14 16:36:22 -03:00
Allisson Azevedo d603b293d8 Add EstimatedTableRowsCount to avoid count in large tables 2018-06-14 16:10:59 -03:00
Dan SosedoffandGitHub 2ce5e9a1e2 Merge pull request #365 from sosedoff/cockroachdb
CockroachDB initial fixes
2018-06-05 22:59:02 -05:00
Dan Sosedoff 108754a8c3 Verify that database name is specified when opening the connection 2018-06-05 22:31:22 -05:00
Dan Sosedoff 8ea8960696 Fix killproc func in cockroach smoke test 2018-06-05 18:58:38 -05:00
Dan Sosedoff 1cbcb73948 Print a correct database server name when starting 2018-06-05 18:28:22 -05:00
Dan Sosedoff e68c7e2c61 Add cockroachdb smoke test 2018-06-05 18:20:58 -05:00
Dan Sosedoff e09f892442 Fix table info call, just return placeholder for now 2018-06-05 16:38:00 -05:00
Dan Sosedoff 56f3b45a4e Fix activity list 2018-06-05 15:48:16 -05:00
Dan Sosedoff 77770112bd Detect server type and version with regular expression 2018-06-05 15:35:19 -05:00
Dan SosedoffandGitHub b18af0b907 Merge pull request #358 from sosedoff/check-for-pgdump
Check if pg_dump is available before running database export
2018-04-26 00:14:17 -05:00
Dan Sosedoff da98261d27 Remove GOVENDOREXPERIMENT and add go 1.10.1 2018-04-26 00:08:30 -05:00
Dan Sosedoff 9fae7c06af Use -race flag with go test 2018-04-26 00:05:32 -05:00
Dan Sosedoff b29c013706 Use go get -u to fetch initial deps 2018-04-26 00:04:48 -05:00
Dan Sosedoff 0b095f43bb Restructure routes 2018-04-25 23:52:22 -05:00
Dan Sosedoff e2be689a05 Check if pg_dump is available before running database export 2018-04-25 23:29:18 -05:00
Dan Sosedoff 7dce0fb4f4 Cleanup whitespace in dockerfile 2018-04-25 23:19:39 -05:00
Dan SosedoffandGitHub a66cf92bd5 Merge pull request #357 from Bizzelicious/patch-1
Update Dockerfile
2018-04-25 23:19:00 -05:00
Jesper EnebergandGitHub 5c8bdf04a1 Update Dockerfile
* Changed MAINTAINER instruction to LABEL since MAINTAINER is deprecated. See https://docs.docker.com/engine/reference/builder/#maintainer-deprecated for more info.
2018-04-26 00:06:18 +02:00
Jesper EnebergandGitHub 315bf650d4 Update Dockerfile
* Removed `apk update`.
* Added `--no-cache` parameter. As of Alpine Linux 3.3 we can utilize the `--no-cache` option for apk. It allows us to install packages with an index that is updated and used on-the-fly and not cached locally. No need to run `apk update`, `apk add --update` and remove `/var/cache/apk/*` when done installing packages.
* Added postgres package so we can utilize pg_dump to dump tables and databases.
2018-04-26 00:00:05 +02:00
Dan Sosedoff f2579a7b37 Bump the pgweb version in docker file 2018-04-24 11:40:03 -05:00
Dan Sosedoff 319b88cedf Update changelog 2018-04-23 12:13:38 -05:00
Dan Sosedoff 9af721176b Release 0.9.12 2018-04-23 12:07:53 -05:00
Dan Sosedoff 113e2f27fb Regenerate bindata 2018-04-23 11:16:02 -05:00
Eric DagenaisandEric Dagenais 7b3e99b599 fixes issue that generates an error message when getting table info, getting table schema, and exporting data for tables with uppercase letters 2018-04-11 06:24:56 -07:00
Dan Sosedoff 2398035e53 Another pass 2018-03-16 20:15:06 -05:00
Dan Sosedoff d153d47604 Another pass at gofmt enforcement 2018-03-16 20:04:08 -05:00
Dan Sosedoff a02c813784 Check files with go fmt in CI 2018-03-16 19:59:58 -05:00
Dan SosedoffandGitHub d72f1c85c1 Merge pull request #347 from sosedoff/specs-refactor
Specs refactor and tweaks
2018-02-28 22:55:45 -06:00
Dan Sosedoff 62fc8a1054 Remove boot2docker mention, its deprecated and no longer supported 2018-02-28 22:35:02 -06:00
Dan Sosedoff e4b1c60ce5 Skip running tests on 10.x branch for now 2018-02-28 22:33:14 -06:00
Dan Sosedoff b620d8f0f2 Specify postgres 10.0 in test-all script 2018-02-22 14:41:45 -06:00
Dan Sosedoff 47500bf92e Specs refactor and tweaks
- Make ParseOptions func to take an arg and return struct so we can test it
- Refactor and add more tests for options parsing
- Run test suite on postgres 10.x branch
- Change format for connection idle timer from float64 to in
2018-02-22 14:20:18 -06:00
Dan Sosedoff 6938eb5f35 Update go versions for testing 2018-02-19 20:37:28 -06:00
Dan Sosedoff 8936adbe1c Merge branch 'go193' 2018-01-30 23:30:12 -06:00
Dan SosedoffandGitHub 121f278713 Merge pull request #343 from sosedoff/list-constraint-name
Include constraint name under "constraints" tab
2018-01-30 23:29:54 -06:00
Dan SosedoffandGitHub 0a0b07a255 Merge pull request #337 from ribice/patch-1
Update bookmarks.go
2018-01-30 23:22:33 -06:00
Dan Sosedoff f44b436bc1 Update travis config per recommendation 2018-01-30 23:20:19 -06:00
Dan Sosedoff 75ab487031 Go 1.9.3 on travis 2018-01-30 22:51:52 -06:00
Dan Sosedoff 133c936d7b License years 2018-01-30 22:42:37 -06:00
Dan Sosedoff b0146a5a76 Update test case to handle the constraint result rows 2018-01-30 22:41:12 -06:00
Dan Sosedoff 9c0d887a3c List name and definition for table constraints 2018-01-30 22:34:54 -06:00
Emir RibićandGitHub 2b2c76bde9 Update bookmarks.go
Fix typo
2018-01-08 10:55:05 +01:00
Dan Sosedoff 43fe84de3a Add link to PG docs about connection strings 2017-12-14 22:49:10 -06:00
Dan Sosedoff 318c33e8ff Changelog 2017-12-07 23:06:28 -06:00
Dan Sosedoff c32784e8ae Release 0.9.11 2017-12-07 22:57:56 -06:00
Dan Sosedoff 898cdd2d65 Bindata gen 2017-12-07 22:56:55 -06:00
Dan SosedoffandGitHub b441151e09 Merge pull request #330 from sosedoff/view-actions
Add views context menu to sidebar
2017-12-01 09:52:21 -06:00
Dan Sosedoff 0ea173ffe5 Add views context menu 2017-11-30 23:38:15 -06:00
Dan SosedoffandGitHub 4b1d0a614c Merge pull request #329 from sosedoff/db-prefix-for-export-files
Add db prefix for table export files
2017-11-30 22:57:44 -06:00
Dan Sosedoff 02d598f406 Add db prefix for table export files 2017-11-30 22:47:42 -06:00
Dan SosedoffandGitHub ae11e469d2 Merge pull request #327 from sosedoff/no-autocomplete-on-connection-url
Disable autocomplete on the connection url string for security reasons
2017-11-30 22:35:05 -06:00
Dan Sosedoff 4f2fac11d3 Disable autocomplete on the connection url string for security reasons 2017-11-30 22:25:20 -06:00
Dan Sosedoff 68bc907bd2 Godep: 1.9 2017-11-28 19:37:25 -06:00
Dan Sosedoff d7e5805024 Rename SQL query tab 2017-11-21 15:33:13 -06:00
Dan SosedoffandGitHub 97e31b6c47 Merge pull request #321 from sosedoff/cors
Add support for CORS
2017-11-18 21:49:07 -06:00
Dan SosedoffandGitHub cb67ef8258 Merge pull request #323 from sosedoff/table-context-reorg
Reorder table context menu
2017-11-16 23:18:39 -06:00
Dan Sosedoff dc3d7303d7 Reorder table context menu 2017-11-16 23:08:18 -06:00
Dan SosedoffandGitHub cdb74c6a19 Merge pull request #322 from sosedoff/fix-custom-results-rows
Hide custom query results counter if query didn't return any rows
2017-11-16 23:07:24 -06:00
Dan Sosedoff cf432e32f7 Hide custom query results counter if query didnt return any rows 2017-11-16 22:21:52 -06:00
Dan Sosedoff c3435e2ece Change CORS to Cors on CLI options, specify origins as an option 2017-11-16 22:10:14 -06:00
Dan Sosedoff b52394a166 Add support for CORS 2017-11-15 15:26:31 -06:00
Dan SosedoffandGitHub d65f8eb0f5 Merge pull request #320 from tylerlong/master
Fix sslmode typo
2017-11-11 10:58:59 -06:00
Tyler LongandGitHub 86acd842b0 Fix sslmode typo
https://www.postgresql.org/docs/9.1/static/libpq-ssl.html
2017-11-11 08:12:27 -06:00
Dan Sosedoff 69b89a0c2a Update docker image version 2017-11-03 22:05:46 -05:00
Dan Sosedoff 2992dd43e8 Update changelog 2017-11-03 22:05:29 -05:00
Dan Sosedoff cb4ecb49eb Release 0.9.10 2017-11-03 21:50:25 -05:00
Dan Sosedoff d51294ce54 Rebuild bindata 2017-11-03 21:49:32 -05:00
Dan SosedoffandGitHub 32eb35f7b2 Merge pull request #311 from krlevkirill/patch-1
Update README - add more badges
2017-10-30 20:52:10 -05:00
krlevkirillandGitHub 80717e6c69 Update README
add more badges
fix link to license
2017-10-29 20:07:28 +02:00
Dan SosedoffandGitHub d123f8fc07 Merge pull request #305 from sosedoff/assets-ci-guard
Trigger CI failure if bindata contains any dev links
2017-10-25 09:39:30 -05:00
Dan Sosedoff 2dcaf4a3a1 Remove bindata check from test-all script 2017-10-24 23:13:22 -05:00
Dan Sosedoff c4cdacc2d8 Add asset check script 2017-10-24 23:11:02 -05:00
Dan Sosedoff 06afa786ef Trigger CI failure if bindata contains any dev links 2017-10-24 22:38:51 -05:00
Dan Sosedoff b8fccab33a Fix broken bindata 2017-10-24 22:31:29 -05:00
Dan Sosedoff ec5a12b0f7 Rename disable connection timeout cli option 2017-10-20 22:18:07 -05:00
Dan SosedoffandGitHub 7b6c5f74d4 Merge pull request #303 from ldinc/issue#282
Issue#282
2017-10-20 22:14:26 -05:00
ldinc 02df9b519f #282: cleared bindata 2017-10-16 00:12:41 +03:00
ldinc 38a3f01868 #282: set flag idle logic to cli-run phase 2017-10-15 01:39:07 +03:00
ldinc a120b58dc2 #282: updated bindata 2017-10-14 14:37:20 +03:00
ldinc 33be6b036d Merge branch 'master' into issue#282 2017-10-14 14:34:32 +03:00
Dan Sosedoff 93211cf073 Merge branch 'EyesBear-master' 2017-10-13 23:04:52 -05:00
Dan Sosedoff 046022221e UI and design tweaks 2017-10-13 23:04:19 -05:00
Jason YeandDan Sosedoff fc01b1db81 Search for database with autocomplete 2017-10-13 22:59:24 -05:00
Dan Sosedoff ed7557c5da Regenerated bindata 2017-10-13 22:34:16 -05:00
Dan SosedoffandGitHub 5d110da392 Merge pull request #302 from ldinc/issue#301
added simple copy to clipboard from data-id prop
2017-10-13 22:33:58 -05:00
ldinc ab443f58d0 #301: change rexexp to String.split() 2017-10-14 03:20:42 +03:00
ldinc d4dde3fe4f #282 fix disabled state per idle timeout 2017-10-14 03:15:31 +03:00
ldinc 3067b06bf6 #282: added timeout flags & tuned client.IsIdle() 2017-10-14 03:08:04 +03:00
ldinc 0e2926cc85 added simple copy to clipboard from data-id prop 2017-10-14 01:23:53 +03:00
Dan SosedoffandGitHub 72b9eb443c Merge pull request #300 from sosedoff/disable-bookmark-on-startup
Dot now allow using startup bookmark in multi-session mode
2017-10-07 12:33:10 -05:00
Dan Sosedoff 4ad70d5392 Tweak wording 2017-10-07 11:30:12 -05:00
Dan Sosedoff d8ce8231cc Dot now allow using startup bookmark in multi-session mode 2017-10-07 09:25:00 -05:00
Dan SosedoffandGitHub 7381ff8589 Merge pull request #296 from sosedoff/ssh-hotfix
Fix panics when sshinfo is not set on bookmarks
2017-10-04 00:31:35 -05:00
Dan Sosedoff 277e2d6cd8 Add an extra test to verify SSHinfoIsEmpty does not panic 2017-10-03 23:03:02 -05:00
Dan Sosedoff 58ae69de0b Fix panics when sshinfo is not set on bookmarks 2017-10-03 23:00:18 -05:00
Dan Sosedoff f73f812cf1 Correction on idle timeout 2017-09-29 00:06:48 -05:00
Dan Sosedoff d1eba4a089 Update docker image version 2017-09-28 21:03:55 -05:00
Dan Sosedoff 502c13c3ce Fix formatting 2017-09-28 20:58:47 -05:00
Dan Sosedoff ee9878ac25 Update changelog 2017-09-28 20:56:24 -05:00
Dan Sosedoff 4cacfc253c Release 0.9.9 2017-09-28 20:42:14 -05:00
Dan SosedoffandGitHub b6167ce422 Merge pull request #279 from sosedoff/disable-ssh
Add new CLI option to disable SSH connections
2017-09-27 00:35:38 -05:00
Dan Sosedoff b1dd4b5308 Add new CLI option to disable SSH connections 2017-09-26 23:56:27 -05:00
Dan SosedoffandGitHub e7a7e02626 Merge pull request #277 from sosedoff/fix-ssh-connections
SSH Connection Fixes
2017-09-26 23:50:12 -05:00
Dan Sosedoff 5e22795828 Allow setting ssh connection key file, fix connection ui 2017-09-26 23:29:28 -05:00
Dan Sosedoff bf423658af Rename examples to config, it's a proper name 2017-09-26 22:12:01 -05:00
Dan SosedoffandGitHub 8bd6f08794 Merge pull request #275 from sosedoff/close-idle-sessions
Automatically close idle sessions
2017-09-22 23:01:01 -05:00
Dan Sosedoff 62d88121cd Automatically close idle sessions 2017-09-22 22:44:32 -05:00
Dan Sosedoff 85e38c390c Merge branch 'dump-test' 2017-09-22 22:17:29 -05:00
Dan Sosedoff 10b6e69dec Do not run CI on go 1.5/1.6 2017-09-22 22:08:32 -05:00
Dan Sosedoff 5d1cc211b0 Add database export test 2017-09-22 22:06:36 -05:00
Dan SosedoffandGitHub 83598f4013 Merge pull request #266 from sosedoff/connect-backend
Add ability to connect with settings from third-party backend
2017-09-21 20:37:46 -05:00
Dan SosedoffandGitHub 76eaa3bc4b Merge pull request #272 from sosedoff/auto-open-when-running
Automatically open pgweb page if its already running
2017-09-21 20:35:21 -05:00
Dan Sosedoff 92ec968d5e Automatically open pgweb page if its already running 2017-09-21 17:16:09 -05:00
Dan Sosedoff 049aa4ec44 Require --connect-backend flag to be set if any other backend flags are specified 2017-09-21 02:06:14 -05:00
Dan Sosedoff 869fd8c6bc Refactor the third-party connect backend functionality 2017-09-21 01:21:26 -05:00
Dan Sosedoff d175b0af34 Add descriptions for cli backend options 2017-09-19 00:45:07 -05:00
Dan Sosedoff 0aff72059b Check for response status when fetching credentials 2017-09-19 00:43:21 -05:00
Dan Sosedoff 168b33d5d5 Test on go1.9 2017-09-19 00:43:21 -05:00
Dan Sosedoff ccb84f585a Fix deps again 2017-09-19 00:43:21 -05:00
Dan Sosedoff 39e7b3f615 Update deps 2017-09-19 00:43:21 -05:00
Dan Sosedoff 73816ff2d7 Add ability to connect with settings from third-party backend 2017-09-19 00:43:21 -05:00
Dan SosedoffandGitHub f5f595ac02 Merge pull request #270 from sosedoff/data-export
Database export
2017-09-19 00:41:04 -05:00
Dan SosedoffandGitHub c0a4e7add1 Merge pull request #271 from sosedoff/pretty-json-default
Fix the JSON beautifier flag
2017-09-16 22:11:41 -05:00
Dan Sosedoff 26ea9fc1b8 Fix the JSON beautifier flag 2017-09-16 21:57:04 -05:00
Dan Sosedoff 3c515fcef3 Update readme: premium features/donations 2017-09-16 21:28:06 -05:00
Dan Sosedoff e771d6f49e Export database or table to sql 2017-09-16 20:32:41 -05:00
Dan SosedoffandGitHub 003a661c08 Merge pull request #268 from sosedoff/fetch-unique-rows
Table headers context menu
2017-09-16 14:22:10 -05:00
Dan Sosedoff 28b0840e69 Move some of the util functions to util.js 2017-09-16 14:03:42 -05:00
Dan Sosedoff 81af2a9a27 Add table results headers context menu 2017-09-16 13:50:22 -05:00
Dan SosedoffandGitHub 206416889c Merge pull request #265 from sosedoff/connection-usage
Record last query timestamp
2017-09-15 17:03:31 -05:00
Dan Sosedoff 6ab30ed308 Update the client last usage timestamp 2017-09-14 00:16:40 -05:00
Dan SosedoffandGitHub 3a9bb58646 Merge pull request #264 from sosedoff/print-version-on-start
Print postgres version on start in a single-session mode
2017-09-14 00:00:21 -05:00
Dan Sosedoff af7e504e4f Print postgres version on start in a single-session mode 2017-09-13 23:42:44 -05:00
Dan SosedoffandGitHub 2178a507c5 Merge pull request #255 from sosedoff/pretty-json-default
Pretty-print JSON results by default
2017-09-13 23:42:19 -05:00
Dan SosedoffandGitHub 7832b5b8b8 Merge pull request #256 from sosedoff/docker-alpine-3.6
Use alpine:3.6 as a docker base image
2017-09-13 23:36:33 -05:00
Dan Sosedoff d786d0018e Make JSON formatting a feature flag, enabled by default 2017-09-13 23:27:54 -05:00
Dan Sosedoff a9efb88fe2 Use alpine:3.6 as a docker base image 2017-08-09 23:11:30 -05:00
Dan Sosedoff e44e66a8e8 Use indent marshaller for JSON results by default 2017-08-04 22:10:12 -05:00
Dan Sosedoff 7d81998577 Update changelog and docker image 2017-08-04 21:58:43 -05:00
Dan Sosedoff 5e748d0cef Release 0.9.8 2017-08-04 21:44:22 -05:00
Dan Sosedoff 4f90561c41 Regenerate bindata 2017-08-04 21:41:15 -05:00
Dan SosedoffandGitHub dc57d5d15f Merge pull request #254 from skabbes/feature/comments
Expose postgres comments to table and columns
2017-08-04 19:40:04 -05:00
Steven Kabbes 2d03aa02e2 test: attempt to fix tests again 2017-08-04 14:16:59 -07:00
Steven Kabbes 8d4d564935 test: fix tests to expect comment column 2017-08-04 14:13:49 -07:00
Steven Kabbes e710dbf720 Add comments to tables and columns 2017-08-04 14:00:52 -07:00
Dan SosedoffandGitHub aba81334a5 Merge pull request #246 from sosedoff/confirm-disconnect
Require confirmation to close the connection
2017-06-05 23:15:31 -05:00
Dan SosedoffandGitHub c53c974713 Merge pull request #247 from sosedoff/postgres-9.6-ci
Test primarily on postgres 9.6
2017-06-05 23:15:17 -05:00
Dan Sosedoff 574f642200 Require confirmation to close the connection 2017-06-05 23:15:03 -05:00
Dan SosedoffandGitHub fd6e4cc189 Merge pull request #245 from sosedoff/for-of-fix
Javascript fixes for IE9-11
2017-06-05 22:46:03 -05:00
Dan Sosedoff c8b90d1286 Test primarily on postgres 9.6 2017-06-05 22:44:57 -05:00
Dan SosedoffandGitHub cbf50299ad Merge pull request #244 from sosedoff/bookmark-ssl-validation
Bookmarks fall back to sslmode=disable if mode is not set or invalid
2017-06-05 22:41:35 -05:00
Dan Sosedoff 26b2cdc194 Fix js on IE9-11 2017-06-05 22:29:44 -05:00
Dan Sosedoff 00ee7084a6 Specify how to run all tests 2017-06-05 21:30:53 -05:00
Dan Sosedoff 75dc1c1548 Fall back to sslmode=disable if ssl mode is not set or invalid in bookmarks 2017-06-05 21:29:09 -05:00
Dan Sosedoff 943963f760 Fix test for mime x-font-woff, pin down golang versions in travis 2017-06-05 21:08:56 -05:00
Dan SosedoffandGitHub 03d3c35a86 Merge pull request #240 from sosedoff/paginate-remember-sort-order
Remember sort column and order for pagination
2017-05-16 22:52:02 -05:00
Dan Sosedoff 3ed194b7bc Remember sort column and order for pagination 2017-05-16 22:34:58 -05:00
Dan Sosedoff c764d53b88 Regenerate bindata 2017-05-16 21:28:24 -05:00
Dan Sosedoff ccc5eddf92 Move all CLI logic into its own package 2017-05-16 21:28:07 -05:00
Dan SosedoffandGitHub 1ff2f7d7ba Merge pull request #238 from bjeanes/patch-1
Show query waiting for 9.6 in activity section
2017-05-10 22:40:49 -05:00
Bo JeanesandGitHub c3f518e5b7 Show query waiting for 9.6 in activity section 2017-05-11 13:30:16 +10:00
Dan SosedoffandGitHub 84cfef8480 Merge pull request #237 from sosedoff/activity-fix
Activity tab fix
2017-05-10 22:14:21 -05:00
Dan Sosedoff bf14066aba Alias procid column to pid so that frontend can properly read it 2017-05-09 23:01:10 -05:00
Dan Sosedoff c592d0063d Set activity queries for different pg versions 2017-05-09 22:51:26 -05:00
Dan Sosedoff e9787440fb Set server version for the client on start up 2017-05-09 21:53:24 -05:00
Dan Sosedoff 1ee02d1451 Add activity query client test 2017-05-09 21:53:24 -05:00
Dan Sosedoff 2378ba894d Reset results table on manual disconnect 2017-05-08 21:37:24 -05:00
Dan SosedoffandGitHub 0fd652f716 Merge pull request #234 from tamird/cockroach
api: properly check for errors
2017-04-30 11:25:38 -05:00
Tamir Duberstein c4a33206ce api: properly check for errors
This results in NPE when cl.Info() returns an error.
2017-04-29 16:56:31 -04:00
Dan SosedoffandGitHub 6289e479a5 Merge pull request #232 from alanfran/patch-1
Removed duplicate usage line in Makefile.
2017-04-27 08:30:15 -05:00
Antonio LanfranchiandGitHub 7d97f6424b Removed duplicate usage line in Makefile.
There were two lines for `make test`.
2017-04-27 05:16:10 -04:00
Dan Sosedoff 383d4049e0 Build docker image on 0.9.7 2017-04-10 10:17:03 -05:00
Dan Sosedoff 5fc844b840 Update changelog for 0.9.7 2017-04-04 22:16:39 -05:00
Dan Sosedoff 4833a76b05 Version bump: 0.9.7 2017-04-04 22:10:45 -05:00
Dan Sosedoff f594dd77fd Update bindata 2017-04-04 22:10:26 -05:00
Dan Sosedoff f99afad7f6 Show build status on linux based off master 2017-04-04 22:06:00 -05:00
Dan Sosedoff 1c327ac252 Use postgres 9.5 by default on travis 2017-02-27 11:52:18 -06:00
Dan Sosedoff 7556726e08 Test on Go 1.8 2017-02-26 15:55:45 -06:00
Dan SosedoffandGitHub 92729f26df Merge pull request #225 from sosedoff/reset-query-rows
Remove table rows when switching to custom query view
2017-02-25 14:43:07 -06:00
Dan Sosedoff 701e536a90 Remove table rows when switching to custom query view 2017-02-25 14:24:57 -06:00
Dan SosedoffandGitHub 1f45a95710 Merge pull request #224 from sosedoff/result-rows-count
Show number of rows rendered for custom sql query
2017-02-25 14:16:25 -06:00
Dan Sosedoff 9f7f83cfd2 Add example initd config 2017-02-25 14:15:03 -06:00
Dan Sosedoff 36fd4871e1 Show number of rows rendered for custom sql query 2017-02-25 14:04:48 -06:00
Dan Sosedoff 7882abea35 Update godeps 2017-02-03 20:29:16 -06:00
Dan SosedoffandGitHub 67fbadabf2 Merge pull request #217 from sosedoff/deps-update
Deps update
2017-01-24 11:16:55 -06:00
Dan SosedoffandGitHub a4f2553fbc Merge pull request #218 from kpashka/master
Ability to override bookmarks directory + typo fix
2017-01-23 19:25:34 -06:00
Pavel Kiselev 4704a8a234 Override in tests 2017-01-24 08:06:12 +07:00
Pavel Kiselev ee547c5899 Override parameter in main.go 2017-01-24 08:04:13 +07:00
Pavel Kiselev 9b1d915dd1 Ability to override bookmarks directory 2017-01-24 07:55:18 +07:00
Dan SosedoffandGitHub f6f40ab707 Merge pull request #214 from jeasoft/patch-1
Example Configuration File for Ubuntu 14.04
2017-01-20 14:15:49 -06:00
Dan Sosedoff 33f3e20849 Use port 30000 to test port availability so it does not interfere with running pgweb instance 2017-01-20 13:41:24 -06:00
Dan Sosedoff 4fcb946f0f Update dependencies 2017-01-20 13:21:18 -06:00
Dan Sosedoff 12976024b7 Update go version in godeps to 1.7 2017-01-20 13:13:00 -06:00
Dan Sosedoff 1f7367edb1 Update license years 2017-01-20 13:05:18 -06:00
Jearel AlcantaraandGitHub 0493d77f46 Example Configuration File for Ubuntu 14.04
Example Configuration File for Ubuntu 14.04 and Upstart, normally in:
/etc/init/
2017-01-12 20:55:50 -04:00
Dan Sosedoff 4af979ac9b Fix issue with long database names in header 2017-01-10 13:44:42 -06:00
Dan SosedoffandGitHub c28695fa18 Merge pull request #212 from fduraffourg/SwitchDb
Fix path rewrite on DB change
2017-01-10 10:49:00 -06:00
Florian Duraffourg 5ffd6d612b Fix path rewrite on DB change 2017-01-10 16:14:50 +01:00
Dan SosedoffandGitHub 577edd86f4 Merge pull request #206 from sosedoff/fix-lock-session
Do not return error when session is locked and database url is blank
2016-11-20 16:55:13 -06:00
Dan Sosedoff 412089948c Do not return error when session is locked and database url is blank 2016-11-20 16:44:33 -06:00
Dan Sosedoff 65773364a1 Update docker version 2016-11-18 12:51:37 -06:00
Dan Sosedoff 91bc5d0d5e Update changelog 2016-11-18 12:40:31 -06:00
Dan Sosedoff b580b2456a Version bump: 0.9.6 2016-11-18 12:36:27 -06:00
Dan Sosedoff 7e27cfe5f8 Regenerate bindata 2016-11-17 21:43:52 -06:00
Dan Sosedoff 4609a3d5f3 Update changelog [ci-skip] 2016-11-15 22:19:24 -06:00
Dan SosedoffandGitHub 20a5646a53 Merge pull request #202 from sosedoff/always-show-sidedebar
Display empty sidebar
2016-11-15 22:06:18 -06:00
Dan Sosedoff 0510634db7 Move bookmark default port parsing to readServerConfig func 2016-11-15 22:01:42 -06:00
Dan Sosedoff 59018287de Fix issue with default port in bookmark; cleanup 2016-11-15 21:58:58 -06:00
Dan Sosedoff e1edc5b550 Resolve conflict with master 2016-11-15 21:51:23 -06:00
Dan SosedoffandGitHub d74bc06cbd Merge pull request #201 from akarki15/Issue-189-take-2
Issue-189 : Allow connecting to bookmarked server from CLI
2016-11-15 21:49:27 -06:00
akarki15 fedde804e3 Remove error return value from ConvertToOptions
ConvertToOptions can't return any error.
2016-11-15 22:03:32 -05:00
Dan Sosedoff 7467553bf2 Fix bindata 2016-11-15 20:55:17 -06:00
Dan Sosedoff 9f8f45db5a Display sidebar even if there are no tables (or any other objects) 2016-11-15 20:55:03 -06:00
akarki15 b1fdbf714b Make assets. 2016-11-12 13:19:09 -05:00
akarki15 68db934507 Use generic typed error pattern
The codebase doesn't yet use the explicitly typed error pattern. To keep
things consistent, lets use the generic error type.
2016-11-12 12:30:15 -05:00
akarki15 1b4902f196 initClient uses bookmark if it exists
if options.Bookmark is set, initClient will now use
it to create a client from the bookmarked settings.

initClientUsingBookmark uses methods introduced in
previous commits. It constructs connection string
to db from Bookmark.Url if it exists. If not, it
uses other fields in Bookmark to construct the
connection string. An implication of this is that
the Url field of Bookmark takes precedence.
2016-11-10 20:06:39 -05:00
akarki15 61dfe67ca8 Add a method to convert bookmark to options
We will use client.NewFromURL to create a client
from stored bookmark in next commit. Since
client.NewFromURL takes in a command.Options, lets
add a method on Bookmark to get corresponding Option.
2016-11-10 20:06:39 -05:00
akarki15 038cb620c6 Set the type of Bookmark.Port to int
We know that a port is a number. Lets enforce that
rule at type level by setting it so.

This commit also adjusts test funcs and helper data
to fit Port's new int type.
2016-11-10 20:06:39 -05:00
akarki15 0e88e3e1f4 Add a func to read bookmark from stored toml file
Given a bookmark path and bookmark name, GetBookmark
returns a Bookmark object that corresponds to the
stored bookmark settings.

In next commits, we will use this method to read
stored bookmarks and create a db client.
2016-11-10 20:06:39 -05:00
akarki15 f4fb5744ef Add method that returns if bookmark's ssh is empty
We will use this method in upcoming commit to create
client from stored bookmark.
2016-11-10 20:06:39 -05:00
Dan SosedoffandGitHub 474c439076 Merge pull request #198 from gallifabio/feature/favor-selected-text
Explain and Export: favor selected text
2016-11-09 10:10:44 -06:00
Fabio Galli 9cb75ec608 Updated bindata file to align with "Explain and Export: favor selected text" change 2016-11-09 09:53:34 +01:00
Dan Sosedoff 560308fd31 Update docker version to 0.9.5 2016-11-08 09:38:01 -06:00
Fabio Galli da9bdb8dcd Favor selected text over the whole ace editor text in "runExplain" and "Export" functionalities 2016-11-08 09:28:23 +01:00
Dan Sosedoff b3723e260f Typo fix 2016-11-07 18:57:25 -06:00
Dan Sosedoff c6bc631327 Add notes about upcoming release in the changelog 2016-11-07 18:56:35 -06:00
Dan SosedoffandGitHub ec013488ff Merge pull request #197 from sosedoff/sticky-tabs
Make tabs sticky: remember tab when switching between tables
2016-11-06 22:19:16 -06:00
Dan Sosedoff 58e5609b59 Make tabs sticky: remember tab when switching between tables 2016-11-06 22:09:46 -06:00
Dan SosedoffandGitHub 00c2b8262b Merge pull request #196 from sosedoff/switch-db
Allow switching between databases
2016-11-06 21:52:58 -06:00
Dan Sosedoff 172f9a82d0 Remove dummy data from database list and tweak the link style 2016-11-06 19:01:38 -06:00
Dan Sosedoff 72325a6cbc Show connect/disconnect button if connection info fetch failed 2016-11-05 22:51:10 -05:00
Dan Sosedoff a3c341b5b4 Show list of databases and allow switching by clicking on current database name 2016-11-05 22:49:17 -05:00
Dan Sosedoff 7d08017c7f Add endpoint to switch active database 2016-11-05 22:23:26 -05:00
Dan SosedoffandGitHub 07ee8448e6 Merge pull request #195 from sosedoff/session-lock
Implement session locking with --lock-session option
2016-11-05 21:58:56 -05:00
Dan Sosedoff 20da36416c Implement session locking with --lock-session option 2016-11-05 21:35:11 -05:00
Dan SosedoffandGitHub 97b612c1b3 Merge pull request #193 from sosedoff/read-only-mode
Read only mode
2016-11-05 21:29:30 -05:00
Dan SosedoffandGitHub 01a69b9bf9 Merge pull request #194 from sosedoff/kill-running-query
Allow stopping any running query from activity panel
2016-11-05 20:51:31 -05:00
Dan Sosedoff d9cb536518 Add security warning 2016-11-05 20:50:56 -05:00
Dan Sosedoff 887acd5a55 Allow stopping any running query from activity panel 2016-11-05 19:53:10 -05:00
Dan Sosedoff da0916b8c7 Add note about skipped windows tests 2016-11-05 18:53:42 -05:00
Dan Sosedoff 8f90ec4173 Add missing history uniqueness test 2016-11-05 17:52:18 -05:00
Dan Sosedoff 2d2bd1d0b1 Add test to verify readonly mode 2016-11-05 17:51:34 -05:00
Dan Sosedoff 661fed0dbb Set default transaction mode to read only with --readonly flag 2016-11-05 17:43:30 -05:00
Dan Sosedoff e3e1d7b012 Remove PGHOST from test-all script for travis 2016-11-04 10:33:37 -05:00
Dan SosedoffandGitHub 1e88d3dc5f Merge pull request #191 from sosedoff/order-escape-query
Add escaping to order statement
2016-11-03 20:08:55 -05:00
Dan SosedoffandGitHub b943f5a51d Merge pull request #190 from sosedoff/fix-pagination-visibility
Show pagination only on rows view
2016-11-03 20:07:46 -05:00
Dan Sosedoff 5b8937ac3a Deprecate Boot2Docker in favor of Docker for Mac for integration testing 2016-11-03 20:05:50 -05:00
Dan Sosedoff d0d84a62cd Add escaping to order statement 2016-11-03 19:56:55 -05:00
Dan Sosedoff 7aa7b5526b Show pagination only on rows view 2016-11-03 19:27:14 -05:00
Dan Sosedoff 812aff9686 Regenerate bindata 2016-10-11 18:12:39 -05:00
Dan SosedoffandGitHub 23e6b02ae4 Merge pull request #186 from SjonHortensius/js-export-btoa-fix
Fix base64 padding with multiple =-signs. An equal amount of dots is …
2016-10-11 18:12:10 -05:00
Sjon Hortensius 2f69538576 Fix base64 padding with multiple =-signs. An equal amount of dots is required in output
instead of merging all =-signs into one dot, which can then not be
decoded. Test string:

SELECT * from version  where "isHelper" order by "order" desc

will be encode to

U0VMRUNUICogZnJvbSB2ZXJzaW9uICB3aGVyZSAiaXNIZWxwZXIiIG9yZGVyIGJ5ICJvcmRlciIgZGVzYw.

missing a dot at the end making it undecodable.

Since =-signs only appear at the end anyway, we don't need the explicit
'$' in the regexp
2016-10-03 16:19:27 +02:00
Dan Sosedoff e52782fe9c Include make docker-release into CI pipeline 2016-10-01 20:43:20 -05:00
Dan Sosedoff 376d2283bf Fix compile tags in makefile due to compiler warnings 2016-10-01 20:32:05 -05:00
Dan Sosedoff 3c3c61fee2 Version bump: 0.9.5 2016-10-01 20:24:49 -05:00
Dan Sosedoff b6a7d8a54a Move systemd script to examples dir 2016-09-28 20:51:54 -05:00
Dan SosedoffandGitHub 9f4c803495 Merge pull request #177 from PSSGCSim/master
Create pgweb.service
2016-09-28 20:51:14 -05:00
Dan Sosedoff d4baaaa0ff Try travis go 1.7 2016-09-28 20:43:33 -05:00
Dan Sosedoff f3bc35ad35 Allow setting auth user and pass using variables 2016-09-28 20:37:07 -05:00
Dan Sosedoff ae8b87fb18 Regenerate bindata 2016-09-05 12:45:44 -05:00
Jan SchwarzrockandGitHub f0406cac8e Create pgweb.service
Add service file for Linux systemd
2016-09-04 11:19:22 +02:00
Dan Sosedoff 12dcc45e88 Formatting; regenerated bindata 2016-09-01 10:06:51 -05:00
Dan Sosedoff 1f39c2e229 Lint: Fix sql statements naming 2016-09-01 10:05:46 -05:00
Dan SosedoffandGitHub 9430d734a7 Merge pull request #175 from cchesk/cleanpr
Fixed broken export to CSV/JSON/XML if hashmark in URL
2016-09-01 10:05:43 -05:00
Dan SosedoffandGitHub 2ca5463d1d Merge pull request #167 from trengrj/limit-schema
Only view schema with USAGE privileges
2016-09-01 09:56:46 -05:00
Aleksander Umov a9fe99b09b Fixed broken export to CSV/JSON/XML if hashmark in URL 2016-08-30 10:50:47 +02:00
Dan Sosedoff dd71d1c49e Add go 1.7 to travis, bump 1.6 2016-08-16 09:46:18 -05:00
Dan Sosedoff ee93fbb120 Fix bindata 2016-08-01 10:32:22 -05:00
Dan Sosedoff ce96579d26 Update dockerfile 2016-07-29 10:43:23 -05:00
Dan Sosedoff 3ae6392a82 Version bump: 0.9.4 2016-07-29 10:37:18 -05:00
Dan Sosedoff ac16a94a92 Update bindata 2016-07-29 10:36:45 -05:00
Dan SosedoffandGitHub 65f5bb2daf Merge pull request #170 from skabbes/hotfix/export_prefix
[UI] fix CSV, JSON, and XML export with --prefix running
2016-07-29 10:21:14 -05:00
Steven Kabbes 22810a96b7 Update bindata 2016-07-27 22:59:58 -07:00
Steven Kabbes 638fd6234e [UI] fix CSV, JSON, and XML export with --prefix running 2016-07-27 22:55:54 -07:00
John Trengrove 838b275c19 Only view schema with USAGE privileges 2016-07-18 13:06:48 +10:00
Dan Sosedoff 6945843c97 Update changelog 2016-07-13 10:08:43 -06:00
Dan Sosedoff 6d53606b22 Update pgweb version in docker image 2016-06-30 10:35:06 -05:00
666 changed files with 283642 additions and 13645 deletions
+10 -6
View File
@@ -1,4 +1,6 @@
sudo: required
dist: trusty
group: deprecated-2017Q4
language: go
@@ -6,14 +8,15 @@ services:
- docker
addons:
postgresql: "9.4"
postgresql: "9.6"
go:
- 1.5.4
- 1.6.2
- 1.10.x
- 1.11.x
env:
- GO15VENDOREXPERIMENT=1
before_install:
- ./script/check_formatting.sh
- ./script/check_assets.sh
install:
- make setup
@@ -21,4 +24,5 @@ install:
script:
- make build
- make test
- PGHOST=127.0.0.1 ./script/test_all.sh
- make test-all
- make docker-release
+127
View File
@@ -1,3 +1,130 @@
## 0.11.1 - 2019-01-28
- Typo fixes
- Add Base64 javascript encoder/decoder to replace deprecated window.atob call, GH-405
- Fix startup error when DATABASE_URL is set, GH-406
- Fix user auto detection when USER env var is not set, GH-408
- Switch bindata dependency to use maintained fork: github.com/go-bindata/go-bindata, GH-409
## 0.11.0 - 2018-12-24
- Tweak sidebar database object counters styles, GH-400
- Do not exit with error if local server is not running, GH-399
- Fix SSH host verification check, GH-398
- Scope activity list to current database only, GH-397
- Show current release version and check for updates, GH-396
- Force switch back to default connection settings view, GH-395
- Fix row count estimation bug, GH-394
- Print out failed query SQL and args with --debug flag, GH-393
## 0.10.0 - 2018-11-28
- Fixes relation not found errors when dealing with table names that have uppercase characters, GH-356
- Dockerfile updates, GH-357
- Check if pg_dump is available before running database export, GH-358
- Improvements to CockroachDB integration, GH-365
- Add EstimatedTableRowsCount to avoid count in large tables, GH-366
- Automatically set table filter option to 'equals' if its not set, GH-370
- Dependencies update and switch to dep, GH-375
- Add column context menu item to get numeric stats, GH-377
- Fix issues with connection string builder, GH-378
- Include rows count to numeric stats view on table column, GH-379
- Make localhost to be a default db host, GH-380
- Clear out connection settings/bookmark on login screen when running in session/connect mode
- Add table row context menu with actions, GH-381
- Allow settings url prefix with URL_PREFIX env var, GH-387
- Fix JSON marshal panic when dealing with NaN values, GH-388
- Fix startup behavior when user did not provide a database name, GH-389
## 0.9.12 - 2018-04-23
- Add link to view database connection string format on login page
- Include constraint name under "constraints" tab, GH-343
- Misc CI and config changes
## 0.9.11 - 2017-12-07
- Fix ssl mode for the connection url in the bookmarks, GH-320
- Add support for CORS, GH-321
- Fix custom query results counter for empty queries, GH-322
- Reorganize the table context menu, GH-323
- Disable database connection string text field autocomplete, GH-327
- Add db prefix to the table export files, GH-329
- Add database view context menu with export actions, GH-330
## 0.9.10 - 2017-11-03
- Make idle connection timeout configurable, [GH-282]
- Fix panics when sshinfo is not set on bookmarks, [GH-296]
- Dot now allow using startup bookmark in multi-session mode, [GH-300]
- Add ability to copy table name from the sidebar, [GH-301]
## 0.9.9 - 2017-09-28
- Automatically format JSON data exports, GH-255
- Update Docker image to alpine:3.6, GH-256
- Print out PostgreSQL server version on start in a single-session mode, GH-264
- Record last query timestamp for the client connection, GH-265
- Add context menu for table headers in browse mode (copy name, see unique values), GH-268
- Add ability to export current database dump, GH-270
- Automatically open pgweb in browser on start if its already running, GH-272
- Connect to the database with credentials provided by a third-party backend, GH-266
- Automatically close idle sessions (no activity in 1 hour), GH-275
- Allow connecting via SSH with a custom private key and other fixes, GH-277
- Add options to disable SSH connections, GH-279
## 0.9.8 - 2017-08-04
- Fixed error checking in the API, GH-234
- Fixed activity tab to support PG 9.x versions, GH-237
- Remember sort column and order for pagination, GH-240
- Use `sslmode=disable` for bookmarks without sslmode option, GH-244
- Javascript fixes for IE9-11, GH-245
- Require confirmation for the disconnect, GH-246
- Clean the results table on manual disconnect
## 0.9.7 - 2017-04-04
- Fixed issue with locked session and empty db url, GH-206
- Fixed path rewrite on DB change, GH-212
- Upgraded dependencies, GH-217
- Added ability to specify bookmarks path, GH-218
- Added counter for the number of rows from a custom SQL query, GH-224
- Added new behavior for removing table rows view on custom SQL query page, GH-225
## 0.9.6 - 2016-11-18
- Fixed bug in query base64-encoding, GH-186
- Fixed rows pagination visibility bug, GH-190
- Fixed issue with query order escaping, GH-191
- Fixed invalid query selection for explain command, GH-198
- Fixed issue with empty sidebar, now it shows empty state, GH-202
- Added new flag --readonly to enable read only transaction mode, GH-193
- Added ability to kill any running query, GH-194
- Added session database connection locking, GH-195
- Added ability to switch between databases, GH-196
- Added feature to keep last selected tab when switching between tables, GH-197
- Added new flag --bookmark (-b) to specify server connection from bookmark, GH-201
## 0.9.5 - 2016-10-01
- Only view schema with USAGE privileges, GH-167
- Fixed broken export to CSV/JSON/XML if hashmark in URL, GH-175
- Added example service configuration for systemd, GH-177
- Allow setting auth user and pass using variables
## 0.9.4 - 2016-07-29
- Fixes CSV/JSON/XML export buttons when pgweb is running with url prefix, GH-170
## 0.9.3 - 2016-06-30
- Uses Go 1.6 for development, GH-155
- Fixes timestamp formatting in CSV export, GH-163
- Included PostgreSQL 9.6 for integration testing
- Switches docker image to Alpine to reduce image size
- Adds support for ARMv5
## 0.9.2 - 2016-03-01
- Fixes bug with unsafe base64 encoded sql queries
+2 -1
View File
@@ -2,5 +2,6 @@
- Create a new git branch
- Make changes
- Run tests: `make test`
- Run tests agains all supported PostreSQL versions: `make test-all` (optional)
- If you change frontend code (js/css) make sure to rebuild assets: `make assets`
- Open a PR
- Open a new pull request
+7 -7
View File
@@ -1,17 +1,17 @@
FROM alpine:3.3
MAINTAINER Dan Sosedoff <dan.sosedoff@gmail.com>
ENV PGWEB_VERSION 0.9.2
FROM alpine:3.6
LABEL maintainer="Dan Sosedoff <dan.sosedoff@gmail.com>"
ENV PGWEB_VERSION 0.11.1
RUN \
apk update && \
apk add ca-certificates && \
apk add --no-cache ca-certificates openssl postgresql wget && \
update-ca-certificates && \
rm -rf /var/cache/apk/* && \
cd /tmp && \
wget https://github.com/sosedoff/pgweb/releases/download/v$PGWEB_VERSION/pgweb_linux_amd64.zip && \
wget -q https://github.com/sosedoff/pgweb/releases/download/v$PGWEB_VERSION/pgweb_linux_amd64.zip && \
unzip pgweb_linux_amd64.zip -d /usr/bin && \
mv /usr/bin/pgweb_linux_amd64 /usr/bin/pgweb && \
rm -f pgweb_linux_amd64.zip
EXPOSE 8081
CMD ["/usr/bin/pgweb", "--bind=0.0.0.0", "--listen=8081"]
CMD ["/usr/bin/pgweb", "--bind=0.0.0.0", "--listen=8081"]
-85
View File
@@ -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
View File
@@ -1,5 +0,0 @@
This directory tree is generated automatically by godep.
Please do not edit.
See https://github.com/tools/godep for more information.
Generated
+186
View File
@@ -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
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2014-2016 Dan Sosedoff <dan.sosedoff@gmail.com>
Copyright (c) 2014-2019 Dan Sosedoff <dan.sosedoff@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
+10 -10
View File
@@ -11,7 +11,6 @@ usage:
@echo "----------------- : -------------------"
@echo "make setup : Install all necessary dependencies"
@echo "make dev : Generate development build"
@echo "make test : Run tests"
@echo "make build : Generate production build for current OS"
@echo "make bootstrap : Install cross-compilation toolchain"
@echo "make release : Generate binaries for all supported OSes"
@@ -26,10 +25,11 @@ usage:
@echo ""
test:
go test -cover ./pkg/...
go test -race -cover ./pkg/...
test-all:
@./script/test_all.sh
@./script/test_cockroach.sh
assets: static/
go-bindata -o pkg/data/bindata.go -pkg data $(BINDATA_OPTS) $(BINDATA_IGNORE) -ignore=[.]gitignore -ignore=[.]gitkeep $<...
@@ -49,12 +49,12 @@ release: clean assets
@echo "Building binaries..."
@gox \
-osarch "$(TARGETS)" \
-ldflags "-X github.com/sosedoff/pgweb/pkg/command.GitCommit $(GIT_COMMIT) -X github.com/sosedoff/pgweb/pkg/command.BuildTime $(BUILD_TIME)" \
-ldflags "-X github.com/sosedoff/pgweb/pkg/command.GitCommit=$(GIT_COMMIT) -X github.com/sosedoff/pgweb/pkg/command.BuildTime=$(BUILD_TIME)" \
-output "./bin/pgweb_{{.OS}}_{{.Arch}}"
@echo "Building ARM binaries..."
GOOS=linux GOARCH=arm GOARM=5 go build \
-ldflags "-X github.com/sosedoff/pgweb/pkg/command.GitCommit $(GIT_COMMIT) -X github.com/sosedoff/pgweb/pkg/command.BuildTime $(BUILD_TIME)" \
-ldflags "-X github.com/sosedoff/pgweb/pkg/command.GitCommit=$(GIT_COMMIT) -X github.com/sosedoff/pgweb/pkg/command.BuildTime=$(BUILD_TIME)" \
-o "./bin/pgweb_linux_arm_v5"
@echo "\nPackaging binaries...\n"
@@ -64,11 +64,11 @@ bootstrap:
gox -build-toolchain
setup:
go get github.com/tools/godep
go get golang.org/x/tools/cmd/cover
go get github.com/mitchellh/gox
go get github.com/jteeuwen/go-bindata/...
godep restore
go get -u github.com/golang/dep/cmd/dep
go get -u golang.org/x/tools/cmd/cover
go get -u github.com/mitchellh/gox
go get -u github.com/go-bindata/go-bindata/...
dep ensure
clean:
@rm -f ./pgweb
@@ -84,4 +84,4 @@ docker-release:
docker-push:
docker push $(DOCKER_RELEASE_TAG)
docker push $(DOCKER_LATEST_TAG)
docker push $(DOCKER_LATEST_TAG)
+18 -3
View File
@@ -3,8 +3,11 @@
Web-based PostgreSQL database browser written in Go.
[![Release](https://img.shields.io/github/release/sosedoff/pgweb.svg?label=Release)](https://github.com/sosedoff/pgweb/releases)
[![Linux Build](https://img.shields.io/travis/sosedoff/pgweb.svg?label=Linux)](https://travis-ci.org/sosedoff/pgweb)
[![Linux Build](https://img.shields.io/travis/sosedoff/pgweb/master.svg?label=Linux)](https://travis-ci.org/sosedoff/pgweb)
[![Windows Build](https://img.shields.io/appveyor/ci/sosedoff/pgweb/master.svg?label=Windows)](https://ci.appveyor.com/project/sosedoff/pgweb)
[![Go Report Card](https://goreportcard.com/badge/github.com/sosedoff/pgweb)](https://goreportcard.com/report/github.com/sosedoff/pgweb)
[![GoDoc](https://godoc.org/github.com/sosedoff/pgweb?status.svg)](https://godoc.org/github.com/sosedoff/pgweb)
[![Docker Pulls](https://img.shields.io/docker/pulls/sosedoff/pgweb.svg)](https://hub.docker.com/r/sosedoff/pgweb/)
## Overview
@@ -32,6 +35,18 @@ application to work with local or remote PostgreSQL databases.
Visit [WIKI](https://github.com/sosedoff/pgweb/wiki) for more details
## Pgweb Pro
Pgweb Pro is the next major version of Pgweb and includes features like:
- Table structure editing
- Data editing (update row content)
- Charting
- History persistence
- Multiple tabs
Please get in touch via: https://goo.gl/forms/euQOGWg5uPdPH70b2
## Demo
Visit https://pgweb-demo.herokuapp.com to see pgweb in action.
@@ -118,6 +133,6 @@ make test-all
## License
The MIT License (MIT)
The MIT License (MIT). See [LICENSE](LICENSE) file for more details.
Copyright (c) 2014-2016 Dan Sosedoff, <dan.sosedoff@gmail.com>
Copyright (c) 2014-2019 Dan Sosedoff, <dan.sosedoff@gmail.com>
+6 -7
View File
@@ -1,7 +1,8 @@
clone_folder: c:\go\src\github.com\sosedoff\pgweb
clone_folder: c:\gopath\src\github.com\sosedoff\pgweb
environment:
GOPATH: c:\gopath
CGO_ENABLED: 0
services:
- postgresql
@@ -13,15 +14,13 @@ install:
- cd %APPVEYOR_BUILD_FOLDER%
- go env
- go version
- go get github.com/tools/godep
- godep get github.com/mitchellh/gox
- godep get github.com/jteeuwen/go-bindata/...
- godep restore
- go get github.com/golang/dep/cmd/dep
- dep ensure
build_script:
- godep go build
- go build
test_script:
- godep go test -cover ./...
- go test -cover ./...
deploy: off
+11
View File
@@ -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
+67
View File
@@ -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
+10
View File
@@ -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
View File
@@ -1,5 +1,5 @@
host = "localhost"
port = "5432"
port = 5432
user = "postgres"
database = "mydatabase"
ssl = "disable"
@@ -2,4 +2,4 @@ host = "localhost"
port = 5432
user = "postgres"
database = "mydatabase"
ssl = "disable"
ssl = "disabled"
+9
View File
@@ -17,6 +17,15 @@ CREATE DATABASE "booktown";
-- Name: DATABASE "booktown" Type: COMMENT Owner:
--
CREATE TABLE "dummies" (
"id" integer NOT NULL,
"isDummy" boolean
);
INSERT INTO "dummies" VALUES (1, true);
INSERT INTO "dummies" VALUES (2, true);
COMMENT ON DATABASE "booktown" IS 'The Book Town Database.';
--
+43
View File
@@ -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;
+2 -135
View File
@@ -1,142 +1,9 @@
package main
import (
"fmt"
"os"
"os/exec"
"os/signal"
"github.com/gin-gonic/gin"
"github.com/sosedoff/pgweb/pkg/api"
"github.com/sosedoff/pgweb/pkg/client"
"github.com/sosedoff/pgweb/pkg/command"
"github.com/sosedoff/pgweb/pkg/connection"
"github.com/sosedoff/pgweb/pkg/util"
"github.com/sosedoff/pgweb/pkg/cli"
)
var options command.Options
func exitWithMessage(message string) {
fmt.Println("Error:", message)
os.Exit(1)
}
func initClient() {
if connection.IsBlank(command.Opts) {
return
}
cl, err := client.New()
if err != nil {
exitWithMessage(err.Error())
}
if command.Opts.Debug {
fmt.Println("Server connection string:", cl.ConnectionString)
}
fmt.Println("Connecting to server...")
err = cl.Test()
if err != nil {
exitWithMessage(err.Error())
}
fmt.Println("Checking database objects...")
_, err = cl.Objects()
if err != nil {
exitWithMessage(err.Error())
}
api.DbClient = cl
}
func initOptions() {
err := command.ParseOptions()
if err != nil {
os.Exit(1)
}
options = command.Opts
if options.Version {
printVersion()
os.Exit(0)
}
printVersion()
}
func printVersion() {
str := fmt.Sprintf("Pgweb v%s", command.VERSION)
if command.GitCommit != "" {
str += fmt.Sprintf(" (git: %s)", command.GitCommit)
}
fmt.Println(str)
}
func startServer() {
router := gin.Default()
// Enable HTTP basic authentication only if both user and password are set
if options.AuthUser != "" && options.AuthPass != "" {
auth := map[string]string{options.AuthUser: options.AuthPass}
router.Use(gin.BasicAuth(auth))
}
api.SetupRoutes(router)
fmt.Println("Starting server...")
go func() {
err := router.Run(fmt.Sprintf("%v:%v", options.HttpHost, options.HttpPort))
if err != nil {
fmt.Println("Cant start server:", err)
os.Exit(1)
}
}()
}
func handleSignals() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, os.Kill)
<-c
}
func openPage() {
url := fmt.Sprintf("http://%v:%v/%s", options.HttpHost, options.HttpPort, options.Prefix)
fmt.Println("To view database open", url, "in browser")
if options.SkipOpen {
return
}
_, err := exec.Command("which", "open").Output()
if err != nil {
return
}
exec.Command("open", url).Output()
}
func main() {
initOptions()
initClient()
if api.DbClient != nil {
defer api.DbClient.Close()
}
if !options.Debug {
gin.SetMode("release")
}
// Print memory usage every 30 seconds with debug flag
if options.Debug {
util.StartProfiler()
}
startServer()
openPage()
handleSignals()
cli.Run()
}
+214 -50
View File
@@ -4,9 +4,12 @@ import (
"encoding/base64"
"errors"
"fmt"
neturl "net/url"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/tuvistavie/securerandom"
"github.com/sosedoff/pgweb/pkg/bookmarks"
"github.com/sosedoff/pgweb/pkg/client"
@@ -16,10 +19,14 @@ import (
)
var (
DbClient *client.Client
// DbClient represents the active database connection in a single-session mode
DbClient *client.Client
// DbSessions represents the mapping for client connections
DbSessions = map[string]*client.Client{}
)
// DB returns a database connection from the client context
func DB(c *gin.Context) *client.Client {
if command.Opts.Sessions {
return DbSessions[getSessionId(c.Request)]
@@ -60,27 +67,76 @@ func GetSessions(c *gin.Context) {
// In debug mode endpoint will return a lot of sensitive information
// like full database connection string and all query history.
if command.Opts.Debug {
c.JSON(200, DbSessions)
successResponse(c, DbSessions)
return
}
successResponse(c, gin.H{"sessions": len(DbSessions)})
}
func ConnectWithBackend(c *gin.Context) {
// Setup a new backend client
backend := Backend{
Endpoint: command.Opts.ConnectBackend,
Token: command.Opts.ConnectToken,
PassHeaders: command.Opts.ConnectHeaders,
}
// Fetch connection credentials
cred, err := backend.FetchCredential(c.Param("resource"), c)
if err != nil {
badRequest(c, err)
return
}
c.JSON(200, map[string]int{"sessions": len(DbSessions)})
// Make the new session
sessionId, err := securerandom.Uuid()
if err != nil {
badRequest(c, err)
return
}
c.Request.Header.Add("x-session-id", sessionId)
// Connect to the database
cl, err := client.NewFromUrl(cred.DatabaseURL, nil)
if err != nil {
badRequest(c, err)
return
}
cl.External = true
// Finalize session seetup
_, err = cl.Info()
if err == nil {
err = setClient(c, cl)
}
if err != nil {
cl.Close()
badRequest(c, err)
return
}
c.Redirect(301, fmt.Sprintf("/%s?session=%s", command.Opts.Prefix, sessionId))
}
func Connect(c *gin.Context) {
if command.Opts.LockSession {
badRequest(c, "Session is locked")
return
}
var sshInfo *shared.SSHInfo
url := c.Request.FormValue("url")
if url == "" {
c.JSON(400, Error{"Url parameter is required"})
badRequest(c, "Url parameter is required")
return
}
opts := command.Options{Url: url}
url, err := connection.FormatUrl(opts)
url, err := connection.FormatURL(opts)
if err != nil {
c.JSON(400, Error{err.Error()})
badRequest(c, err)
return
}
@@ -90,67 +146,138 @@ func Connect(c *gin.Context) {
cl, err := client.NewFromUrl(url, sshInfo)
if err != nil {
c.JSON(400, Error{err.Error()})
badRequest(c, err)
return
}
err = cl.Test()
if err != nil {
c.JSON(400, Error{err.Error()})
badRequest(c, err)
return
}
info, err := cl.Info()
if err == nil {
err = setClient(c, cl)
if err != nil {
cl.Close()
c.JSON(400, Error{err.Error()})
return
}
}
if err != nil {
cl.Close()
badRequest(c, err)
return
}
c.JSON(200, info.Format()[0])
successResponse(c, info.Format()[0])
}
func SwitchDb(c *gin.Context) {
if command.Opts.LockSession {
badRequest(c, "Session is locked")
return
}
name := c.Request.URL.Query().Get("db")
if name == "" {
name = c.Request.FormValue("db")
}
if name == "" {
badRequest(c, "Database name is not provided")
return
}
conn := DB(c)
if conn == nil {
badRequest(c, "Not connected")
return
}
// Do not allow switching databases for connections from third-party backends
if conn.External {
badRequest(c, "Session is locked")
return
}
currentUrl, err := neturl.Parse(conn.ConnectionString)
if err != nil {
badRequest(c, "Unable to parse current connection string")
return
}
currentUrl.Path = name
cl, err := client.NewFromUrl(currentUrl.String(), nil)
if err != nil {
badRequest(c, err)
return
}
err = cl.Test()
if err != nil {
badRequest(c, err)
return
}
info, err := cl.Info()
if err == nil {
err = setClient(c, cl)
}
if err != nil {
cl.Close()
badRequest(c, err)
return
}
conn.Close()
successResponse(c, info.Format()[0])
}
func Disconnect(c *gin.Context) {
if command.Opts.LockSession {
badRequest(c, "Session is locked")
return
}
conn := DB(c)
if conn == nil {
c.JSON(400, Error{"Not connected"})
badRequest(c, "Not connected")
return
}
err := conn.Close()
if err != nil {
c.JSON(400, Error{err.Error()})
badRequest(c, err)
return
}
c.JSON(200, map[string]bool{"success": true})
successResponse(c, gin.H{"success": true})
}
func GetDatabases(c *gin.Context) {
conn := DB(c)
if conn.External {
errorResponse(c, 403, "Not permitted")
return
}
names, err := DB(c).Databases()
serveResult(names, err, c)
serveResult(c, names, err)
}
func GetObjects(c *gin.Context) {
result, err := DB(c).Objects()
if err != nil {
c.JSON(400, NewError(err))
badRequest(c, err)
return
}
objects := client.ObjectsFromResult(result)
c.JSON(200, objects)
successResponse(c, client.ObjectsFromResult(result))
}
func RunQuery(c *gin.Context) {
query := cleanQuery(c.Request.FormValue("query"))
if query == "" {
c.JSON(400, NewError(errors.New("Query parameter is missing")))
badRequest(c, "Query parameter is missing")
return
}
@@ -161,7 +288,7 @@ func ExplainQuery(c *gin.Context) {
query := cleanQuery(c.Request.FormValue("query"))
if query == "" {
c.JSON(400, NewError(errors.New("Query parameter is missing")))
badRequest(c, "Query parameter is missing")
return
}
@@ -170,7 +297,7 @@ func ExplainQuery(c *gin.Context) {
func GetSchemas(c *gin.Context) {
res, err := DB(c).Schemas()
serveResult(res, err, c)
serveResult(c, res, err)
}
func GetTable(c *gin.Context) {
@@ -183,19 +310,19 @@ func GetTable(c *gin.Context) {
res, err = DB(c).Table(c.Params.ByName("table"))
}
serveResult(res, err, c)
serveResult(c, res, err)
}
func GetTableRows(c *gin.Context) {
offset, err := parseIntFormValue(c, "offset", 0)
if err != nil {
c.JSON(400, NewError(err))
badRequest(c, err)
return
}
limit, err := parseIntFormValue(c, "limit", 100)
if err != nil {
c.JSON(400, NewError(err))
badRequest(c, err)
return
}
@@ -209,13 +336,13 @@ func GetTableRows(c *gin.Context) {
res, err := DB(c).TableRows(c.Params.ByName("table"), opts)
if err != nil {
c.JSON(400, NewError(err))
badRequest(c, err)
return
}
countRes, err := DB(c).TableRowsCount(c.Params.ByName("table"), opts)
if err != nil {
c.JSON(400, NewError(err))
badRequest(c, err)
return
}
@@ -235,48 +362,49 @@ func GetTableRows(c *gin.Context) {
PerPage: numFetch,
}
serveResult(res, err, c)
serveResult(c, res, err)
}
func GetTableInfo(c *gin.Context) {
res, err := DB(c).TableInfo(c.Params.ByName("table"))
if err != nil {
c.JSON(400, NewError(err))
return
if err == nil {
successResponse(c, res.Format()[0])
} else {
badRequest(c, err)
}
c.JSON(200, res.Format()[0])
}
func GetHistory(c *gin.Context) {
c.JSON(200, DB(c).History)
successResponse(c, DB(c).History)
}
func GetConnectionInfo(c *gin.Context) {
res, err := DB(c).Info()
if err != nil {
c.JSON(400, NewError(err))
badRequest(c, err)
return
}
c.JSON(200, res.Format()[0])
info := res.Format()[0]
info["session_lock"] = command.Opts.LockSession
successResponse(c, info)
}
func GetActivity(c *gin.Context) {
res, err := DB(c).Activity()
serveResult(res, err, c)
serveResult(c, res, err)
}
func GetTableIndexes(c *gin.Context) {
res, err := DB(c).TableIndexes(c.Params.ByName("table"))
serveResult(res, err, c)
serveResult(c, res, err)
}
func GetTableConstraints(c *gin.Context) {
res, err := DB(c).TableConstraints(c.Params.ByName("table"))
serveResult(res, err, c)
serveResult(c, res, err)
}
func HandleQuery(query string, c *gin.Context) {
@@ -287,7 +415,7 @@ func HandleQuery(query string, c *gin.Context) {
result, err := DB(c).Query(query)
if err != nil {
c.JSON(400, NewError(err))
badRequest(c, err)
return
}
@@ -306,7 +434,7 @@ func HandleQuery(query string, c *gin.Context) {
case "csv":
c.Data(200, "text/csv", result.CSV())
case "json":
c.Data(200, "applicaiton/json", result.JSON())
c.Data(200, "application/json", result.JSON())
case "xml":
c.XML(200, result)
default:
@@ -315,16 +443,52 @@ func HandleQuery(query string, c *gin.Context) {
}
func GetBookmarks(c *gin.Context) {
bookmarks, err := bookmarks.ReadAll(bookmarks.Path())
serveResult(bookmarks, err, c)
bookmarks, err := bookmarks.ReadAll(bookmarks.Path(command.Opts.BookmarksDir))
serveResult(c, bookmarks, err)
}
func GetInfo(c *gin.Context) {
info := map[string]string{
"version": command.VERSION,
successResponse(c, gin.H{
"version": command.Version,
"git_sha": command.GitCommit,
"build_time": command.BuildTime,
})
}
// Export database or table data
func DataExport(c *gin.Context) {
db := DB(c)
info, err := db.Info()
if err != nil {
badRequest(c, err)
return
}
c.JSON(200, info)
dump := client.Dump{
Table: strings.TrimSpace(c.Request.FormValue("table")),
}
// If pg_dump is not available the following code will not show an error in browser.
// This is due to the headers being written first.
if !dump.CanExport() {
badRequest(c, "pg_dump is not found")
return
}
formattedInfo := info.Format()[0]
filename := formattedInfo["current_database"].(string)
if dump.Table != "" {
filename = filename + "_" + dump.Table
}
c.Header(
"Content-Disposition",
fmt.Sprintf(`attachment; filename="%s.sql.gz"`, filename),
)
err = dump.Export(db.ConnectionString, c.Writer)
if err != nil {
badRequest(c, err)
}
}
-2
View File
@@ -17,8 +17,6 @@ func Test_assetContentType(t *testing.T) {
"foo.gif": "image/gif",
"foo.eot": "application/vnd.ms-fontobject",
"foo.svg": "image/svg+xml",
"foo.ttf": "application/x-font-ttf",
"foo.woff": "application/x-font-woff",
"foo.foo": "text/plain; charset=utf-8",
"foo": "text/plain; charset=utf-8",
}
+74
View File
@@ -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
View File
@@ -10,33 +10,37 @@ import (
"github.com/gin-gonic/gin"
"github.com/sosedoff/pgweb/pkg/data"
"github.com/sosedoff/pgweb/pkg/shared"
)
var extraMimeTypes = map[string]string{
".icon": "image-x-icon",
".ttf": "application/x-font-ttf",
".woff": "application/x-font-woff",
".eot": "application/vnd.ms-fontobject",
".svg": "image/svg+xml",
".html": "text/html; charset-utf-8",
}
var (
// Mime types definitions
extraMimeTypes = map[string]string{
".icon": "image-x-icon",
".ttf": "application/x-font-ttf",
".woff": "application/x-font-woff",
".eot": "application/vnd.ms-fontobject",
".svg": "image/svg+xml",
".html": "text/html; charset-utf-8",
}
// Paths that dont require database connection
var allowedPaths = map[string]bool{
"/api/sessions": true,
"/api/info": true,
"/api/connect": true,
"/api/bookmarks": true,
"/api/history": true,
}
// Paths that dont require database connection
allowedPaths = map[string]bool{
"/api/sessions": true,
"/api/info": true,
"/api/connect": true,
"/api/bookmarks": true,
"/api/history": true,
}
// List of characters replaced by javascript code to make queries url-safe.
var base64subs = map[string]string{
"-": "+",
"_": "/",
".": "=",
}
// List of characters replaced by javascript code to make queries url-safe.
base64subs = map[string]string{
"-": "+",
"_": "/",
".": "=",
}
)
type Error struct {
Message string `json:"error"`
@@ -115,6 +119,7 @@ func parseSshInfo(c *gin.Context) *shared.SSHInfo {
Port: c.Request.FormValue("ssh_port"),
User: c.Request.FormValue("ssh_user"),
Password: c.Request.FormValue("ssh_password"),
Key: c.Request.FormValue("ssh_key"),
}
if info.Port == "" {
@@ -138,3 +143,48 @@ func assetContentType(name string) string {
return result
}
func serveStaticAsset(path string, c *gin.Context) {
data, err := data.Asset("static" + path)
if err != nil {
c.String(400, err.Error())
return
}
c.Data(200, assetContentType(path), data)
}
// Send a query result to client
func serveResult(c *gin.Context, result interface{}, err interface{}) {
if err == nil {
successResponse(c, result)
} else {
badRequest(c, err)
}
}
// Send successful response back to client
func successResponse(c *gin.Context, data interface{}) {
c.JSON(200, data)
}
// Send an error response back to client
func errorResponse(c *gin.Context, status int, err interface{}) {
var message interface{}
switch v := err.(type) {
case error:
message = v.Error()
case string:
message = v
default:
message = v
}
c.AbortWithStatusJSON(status, gin.H{"status": status, "error": message})
}
// Send a bad request (http 400) back to client
func badRequest(c *gin.Context, err interface{}) {
errorResponse(c, 400, err)
}
+34
View File
@@ -1,10 +1,13 @@
package api
import (
"errors"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
@@ -36,3 +39,34 @@ func Test_getSessionId(t *testing.T) {
req.URL, _ = url.Parse("http://foobar/?_session_id=token")
assert.Equal(t, "token", getSessionId(req))
}
func Test_serveResult(t *testing.T) {
server := gin.Default()
server.GET("/good", func(c *gin.Context) {
serveResult(c, gin.H{"foo": "bar"}, nil)
})
server.GET("/bad", func(c *gin.Context) {
serveResult(c, nil, errors.New("message"))
})
server.GET("/nodata", func(c *gin.Context) {
serveResult(c, nil, nil)
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/good", nil)
server.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, `{"foo":"bar"}`, w.Body.String())
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/bad", nil)
server.ServeHTTP(w, req)
assert.Equal(t, 400, w.Code)
assert.Equal(t, `{"error":"message","status":400}`, w.Body.String())
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/nodata", nil)
server.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, `null`, w.Body.String())
}
+16 -27
View File
@@ -7,24 +7,23 @@ import (
"github.com/gin-gonic/gin"
"github.com/sosedoff/pgweb/pkg/command"
"github.com/sosedoff/pgweb/pkg/data"
)
// Middleware function to check database connection status before running queries
// Middleware to check database connection status before running queries
func dbCheckMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
path := strings.Replace(c.Request.URL.Path, command.Opts.Prefix, "", -1)
if allowedPaths[path] == true {
// Allow whitelisted paths
if allowedPaths[path] {
c.Next()
return
}
// We dont care about sessions unless they're enabled
// Check if session exists in single-session mode
if !command.Opts.Sessions {
if DbClient == nil {
c.JSON(400, Error{"Not connected"})
c.Abort()
badRequest(c, "Not connected")
return
}
@@ -32,17 +31,17 @@ func dbCheckMiddleware() gin.HandlerFunc {
return
}
// Determine session ID from the client request
sessionId := getSessionId(c.Request)
if sessionId == "" {
c.JSON(400, Error{"Session ID is required"})
c.Abort()
badRequest(c, "Session ID is required")
return
}
// Determine the database connection handle for the session
conn := DbSessions[sessionId]
if conn == nil {
c.JSON(400, Error{"Not connected"})
c.Abort()
badRequest(c, "Not connected")
return
}
@@ -50,7 +49,7 @@ func dbCheckMiddleware() gin.HandlerFunc {
}
}
// Middleware function to print out request parameters and body for debugging
// Middleware to print out request parameters and body for debugging
func requestInspectMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
err := c.Request.ParseForm()
@@ -58,21 +57,11 @@ func requestInspectMiddleware() gin.HandlerFunc {
}
}
func serveStaticAsset(path string, c *gin.Context) {
data, err := data.Asset("static" + path)
if err != nil {
c.String(400, err.Error())
return
// Middleware to inject CORS headers
func corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
c.Header("Access-Control-Expose-Headers", "*")
c.Header("Access-Control-Allow-Origin", command.Opts.CorsOrigin)
}
c.Data(200, assetContentType(path), data)
}
func serveResult(result interface{}, err error, c *gin.Context) {
if err != nil {
c.JSON(400, NewError(err))
return
}
c.JSON(200, result)
}
+34 -29
View File
@@ -10,41 +10,46 @@ func SetupMiddlewares(group *gin.RouterGroup) {
group.Use(requestInspectMiddleware())
}
if command.Opts.Cors {
group.Use(corsMiddleware())
}
group.Use(dbCheckMiddleware())
}
func SetupRoutes(router *gin.Engine) {
group := router.Group(command.Opts.Prefix)
root := router.Group(command.Opts.Prefix)
group.GET("/", GetHome)
group.GET("/static/*path", GetAsset)
root.GET("/", GetHome)
root.GET("/static/*path", GetAsset)
root.GET("/connect/:resource", ConnectWithBackend)
api := group.Group("/api")
{
SetupMiddlewares(api)
api := root.Group("/api")
SetupMiddlewares(api)
if command.Opts.Sessions {
api.GET("/sessions", GetSessions)
}
api.GET("/info", GetInfo)
api.POST("/connect", Connect)
api.POST("/disconnect", Disconnect)
api.GET("/databases", GetDatabases)
api.GET("/connection", GetConnectionInfo)
api.GET("/activity", GetActivity)
api.GET("/schemas", GetSchemas)
api.GET("/objects", GetObjects)
api.GET("/tables/:table", GetTable)
api.GET("/tables/:table/rows", GetTableRows)
api.GET("/tables/:table/info", GetTableInfo)
api.GET("/tables/:table/indexes", GetTableIndexes)
api.GET("/tables/:table/constraints", GetTableConstraints)
api.GET("/query", RunQuery)
api.POST("/query", RunQuery)
api.GET("/explain", ExplainQuery)
api.POST("/explain", ExplainQuery)
api.GET("/history", GetHistory)
api.GET("/bookmarks", GetBookmarks)
if command.Opts.Sessions {
api.GET("/sessions", GetSessions)
}
api.GET("/info", GetInfo)
api.POST("/connect", Connect)
api.POST("/disconnect", Disconnect)
api.POST("/switchdb", SwitchDb)
api.GET("/databases", GetDatabases)
api.GET("/connection", GetConnectionInfo)
api.GET("/activity", GetActivity)
api.GET("/schemas", GetSchemas)
api.GET("/objects", GetObjects)
api.GET("/tables/:table", GetTable)
api.GET("/tables/:table/rows", GetTableRows)
api.GET("/tables/:table/info", GetTableInfo)
api.GET("/tables/:table/indexes", GetTableIndexes)
api.GET("/tables/:table/constraints", GetTableConstraints)
api.GET("/query", RunQuery)
api.POST("/query", RunQuery)
api.GET("/explain", ExplainQuery)
api.POST("/explain", ExplainQuery)
api.GET("/history", GetHistory)
api.GET("/bookmarks", GetBookmarks)
api.GET("/export", DataExport)
}
+41
View File
@@ -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
View File
@@ -9,18 +9,35 @@ import (
"github.com/BurntSushi/toml"
"github.com/mitchellh/go-homedir"
"github.com/sosedoff/pgweb/pkg/command"
"github.com/sosedoff/pgweb/pkg/shared"
)
type Bookmark struct {
Url string `json:"url"` // Postgres connection URL
Host string `json:"host"` // Server hostname
Port string `json:"port"` // Server port
User string `json:"user"` // Database user
Password string `json:"password"` // User password
Database string `json:"database"` // Database name
Ssl string `json:"ssl"` // Connection SSL mode
Ssh shared.SSHInfo `json:"ssh"` // SSH tunnel config
Url string `json:"url"` // Postgres connection URL
Host string `json:"host"` // Server hostname
Port int `json:"port"` // Server port
User string `json:"user"` // Database user
Password string `json:"password"` // User password
Database string `json:"database"` // Database name
Ssl string `json:"ssl"` // Connection SSL mode
Ssh *shared.SSHInfo `json:"ssh"` // SSH tunnel config
}
func (b Bookmark) SSHInfoIsEmpty() bool {
return b.Ssh == nil || b.Ssh.User == "" && b.Ssh.Host == "" && b.Ssh.Port == ""
}
func (b Bookmark) ConvertToOptions() command.Options {
return command.Options{
Url: b.Url,
Host: b.Host,
Port: b.Port,
User: b.User,
Pass: b.Password,
DbName: b.Database,
Ssl: b.Ssl,
}
}
func readServerConfig(path string) (Bookmark, error) {
@@ -32,6 +49,32 @@ func readServerConfig(path string) (Bookmark, error) {
}
_, err = toml.Decode(string(buff), &bookmark)
if bookmark.Port == 0 {
bookmark.Port = 5432
}
// List of all supported postgres modes
modes := []string{"disable", "allow", "prefer", "require", "verify-ca", "verify-full"}
valid := false
for _, mode := range modes {
if bookmark.Ssl == mode {
valid = true
break
}
}
// Fall back to a default mode if mode is not set or invalid
// Typical typo: ssl mode set to "disabled"
if bookmark.Ssl == "" || !valid {
bookmark.Ssl = "disable"
}
if bookmark.Ssh != nil && bookmark.Ssh.Port == "" {
bookmark.Ssh.Port = "22"
}
return bookmark, err
}
@@ -40,9 +83,13 @@ func fileBasename(path string) string {
return strings.Replace(filename, filepath.Ext(path), "", 1)
}
func Path() string {
path, _ := homedir.Dir()
return fmt.Sprintf("%s/.pgweb/bookmarks", path)
func Path(overrideDir string) string {
if overrideDir == "" {
path, _ := homedir.Dir()
return fmt.Sprintf("%s/.pgweb/bookmarks", path)
}
return overrideDir
}
func ReadAll(path string) (map[string]Bookmark, error) {
@@ -72,3 +119,17 @@ func ReadAll(path string) (map[string]Bookmark, error) {
return results, nil
}
func GetBookmark(bookmarkPath string, bookmarkName string) (Bookmark, error) {
bookmarks, err := ReadAll(bookmarkPath)
if err != nil {
return Bookmark{}, err
}
bookmark, ok := bookmarks[bookmarkName]
if !ok {
return Bookmark{}, fmt.Errorf("couldn't find a bookmark with name %s", bookmarkName)
}
return bookmark, nil
}
+81 -11
View File
@@ -3,6 +3,8 @@ package bookmarks
import (
"testing"
"github.com/sosedoff/pgweb/pkg/command"
"github.com/sosedoff/pgweb/pkg/shared"
"github.com/stretchr/testify/assert"
)
@@ -12,24 +14,23 @@ func Test_Invalid_Bookmark_Files(t *testing.T) {
_, err = readServerConfig("../../data/invalid.toml")
assert.Error(t, err)
assert.Equal(t, "Near line 1, key 'invalid encoding': Near line 2: Expected key separator '=', but got '\\n' instead.", err.Error())
_, err = readServerConfig("../../data/invalid_port.toml")
assert.Error(t, err)
assert.Equal(t, "Type mismatch for 'bookmarks.Bookmark.Port': Expected string but found 'int64'.", err.Error())
assert.Equal(t, "Near line 1 (last key parsed 'invalid'): expected key separator '=', but got 'e' instead", err.Error())
}
func Test_Bookmark(t *testing.T) {
bookmark, err := readServerConfig("../../data/bookmark.toml")
assert.Equal(t, nil, err)
assert.Equal(t, "localhost", bookmark.Host)
assert.Equal(t, "5432", bookmark.Port)
assert.Equal(t, 5432, bookmark.Port)
assert.Equal(t, "postgres", bookmark.User)
assert.Equal(t, "mydatabase", bookmark.Database)
assert.Equal(t, "disable", bookmark.Ssl)
assert.Equal(t, "", bookmark.Password)
assert.Equal(t, "", bookmark.Url)
bookmark, err = readServerConfig("../../data/bookmark_invalid_ssl.toml")
assert.Equal(t, nil, err)
assert.Equal(t, "disable", bookmark.Ssl)
}
func Test_Bookmark_URL(t *testing.T) {
@@ -38,15 +39,15 @@ func Test_Bookmark_URL(t *testing.T) {
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://username:password@host:port/database?sslmode=disable", bookmark.Url)
assert.Equal(t, "", bookmark.Host)
assert.Equal(t, "", bookmark.Port)
assert.Equal(t, 5432, bookmark.Port)
assert.Equal(t, "", bookmark.User)
assert.Equal(t, "", bookmark.Database)
assert.Equal(t, "", bookmark.Ssl)
assert.Equal(t, "disable", bookmark.Ssl)
assert.Equal(t, "", bookmark.Password)
}
func Test_Bookmarks_Path(t *testing.T) {
assert.NotEqual(t, "/.pgweb/bookmarks", Path())
assert.NotEqual(t, "/.pgweb/bookmarks", Path(""))
}
func Test_Basename(t *testing.T) {
@@ -67,5 +68,74 @@ func Test_ReadBookmarks(t *testing.T) {
bookmarks, err := ReadAll("../../data")
assert.Equal(t, nil, err)
assert.Equal(t, 2, len(bookmarks))
assert.Equal(t, 3, len(bookmarks))
}
func Test_GetBookmark(t *testing.T) {
expBookmark := Bookmark{
Host: "localhost",
Port: 5432,
User: "postgres",
Password: "",
Database: "mydatabase",
Ssl: "disable",
}
b, err := GetBookmark("../../data", "bookmark")
if assert.NoError(t, err) {
assert.Equal(t, expBookmark, b)
}
_, err = GetBookmark("../../data", "bar")
expErrStr := "couldn't find a bookmark with name bar"
assert.Equal(t, expErrStr, err.Error())
_, err = GetBookmark("foo", "bookmark")
assert.Error(t, err)
}
func Test_Bookmark_SSHInfoIsEmpty(t *testing.T) {
emptySSH := &shared.SSHInfo{
Host: "",
Port: "",
User: "",
}
populatedSSH := &shared.SSHInfo{
Host: "localhost",
Port: "8080",
User: "postgres",
}
b := Bookmark{Ssh: nil}
assert.True(t, b.SSHInfoIsEmpty())
b = Bookmark{Ssh: emptySSH}
assert.True(t, b.SSHInfoIsEmpty())
b.Ssh = populatedSSH
assert.False(t, b.SSHInfoIsEmpty())
}
func Test_ConvertToOptions(t *testing.T) {
b := Bookmark{
Url: "postgres://username:password@host:port/database?sslmode=disable",
Host: "localhost",
Port: 5432,
User: "postgres",
Password: "password",
Database: "mydatabase",
Ssl: "disable",
}
expOpt := command.Options{
Url: "postgres://username:password@host:port/database?sslmode=disable",
Host: "localhost",
Port: 5432,
User: "postgres",
Pass: "password",
DbName: "mydatabase",
Ssl: "disable",
}
opt := b.ConvertToOptions()
assert.Equal(t, expOpt, opt)
}
+221
View File
@@ -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
View File
@@ -2,13 +2,16 @@ package client
import (
"fmt"
"log"
neturl "net/url"
"reflect"
"regexp"
"strings"
_ "github.com/lib/pq"
"time"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
"github.com/sosedoff/pgweb/pkg/command"
"github.com/sosedoff/pgweb/pkg/connection"
"github.com/sosedoff/pgweb/pkg/history"
@@ -16,9 +19,21 @@ import (
"github.com/sosedoff/pgweb/pkg/statements"
)
var (
postgresSignature = regexp.MustCompile(`(?i)postgresql ([\d\.]+)\s`)
postgresType = "PostgreSQL"
cockroachSignature = regexp.MustCompile(`(?i)cockroachdb ccl v([\d\.]+)\s`)
cockroachType = "CockroachDB"
)
type Client struct {
db *sqlx.DB
tunnel *Tunnel
serverVersion string
serverType string
lastQueryTime time.Time
External bool
History []history.Record `json:"history"`
ConnectionString string `json:"connection_string"`
}
@@ -41,7 +56,7 @@ func getSchemaAndTable(str string) (string, string) {
}
func New() (*Client, error) {
str, err := connection.BuildString(command.Opts)
str, err := connection.BuildStringFromOptions(command.Opts)
if command.Opts.Debug && str != "" {
fmt.Println("Creating a new client for:", str)
@@ -62,6 +77,7 @@ func New() (*Client, error) {
History: history.New(),
}
client.setServerVersion()
return &client, nil
}
@@ -69,6 +85,9 @@ func NewFromUrl(url string, sshInfo *shared.SSHInfo) (*Client, error) {
var tunnel *Tunnel
if sshInfo != nil {
if command.Opts.DisableSSH {
return nil, fmt.Errorf("ssh connections are disabled")
}
if command.Opts.Debug {
fmt.Println("Opening SSH tunnel for:", sshInfo)
}
@@ -101,6 +120,11 @@ func NewFromUrl(url string, sshInfo *shared.SSHInfo) (*Client, error) {
fmt.Println("Creating a new client for:", url)
}
uri, err := neturl.Parse(url)
if err == nil && uri.Path == "" {
return nil, fmt.Errorf("Database name is not provided")
}
db, err := sqlx.Open("postgres", url)
if err != nil {
return nil, err
@@ -113,36 +137,62 @@ func NewFromUrl(url string, sshInfo *shared.SSHInfo) (*Client, error) {
History: history.New(),
}
client.setServerVersion()
return &client, nil
}
func (client *Client) setServerVersion() {
res, err := client.query("SELECT version()")
if err != nil || len(res.Rows) < 1 {
return
}
version := res.Rows[0][0].(string)
// Detect postgresql
matches := postgresSignature.FindAllStringSubmatch(version, 1)
if len(matches) > 0 {
client.serverType = postgresType
client.serverVersion = matches[0][1]
return
}
// Detect cockroachdb
matches = cockroachSignature.FindAllStringSubmatch(version, 1)
if len(matches) > 0 {
client.serverType = cockroachType
client.serverVersion = matches[0][1]
return
}
}
func (client *Client) Test() error {
return client.db.Ping()
}
func (client *Client) Info() (*Result, error) {
return client.query(statements.PG_INFO)
return client.query(statements.Info)
}
func (client *Client) Databases() ([]string, error) {
return client.fetchRows(statements.PG_DATABASES)
return client.fetchRows(statements.Databases)
}
func (client *Client) Schemas() ([]string, error) {
return client.fetchRows(statements.PG_SCHEMAS)
return client.fetchRows(statements.Schemas)
}
func (client *Client) Objects() (*Result, error) {
return client.query(statements.PG_OBJECTS)
return client.query(statements.Objects)
}
func (client *Client) Table(table string) (*Result, error) {
schema, table := getSchemaAndTable(table)
return client.query(statements.PG_TABLE_SCHEMA, schema, table)
return client.query(statements.TableSchema, schema, table)
}
func (client *Client) MaterializedView(name string) (*Result, error) {
return client.query(statements.PG_MATERIALIZED_VIEW_SCHEMA, name)
return client.query(statements.MaterializedView, name)
}
func (client *Client) TableRows(table string, opts RowsOptions) (*Result, error) {
@@ -158,7 +208,7 @@ func (client *Client) TableRows(table string, opts RowsOptions) (*Result, error)
opts.SortOrder = "ASC"
}
sql += fmt.Sprintf(" ORDER BY %s %s", opts.SortColumn, opts.SortOrder)
sql += fmt.Sprintf(` ORDER BY "%s" %s`, opts.SortColumn, opts.SortOrder)
}
if opts.Limit > 0 {
@@ -172,9 +222,34 @@ func (client *Client) TableRows(table string, opts RowsOptions) (*Result, error)
return client.query(sql)
}
func (client *Client) TableRowsCount(table string, opts RowsOptions) (*Result, error) {
func (client *Client) EstimatedTableRowsCount(table string, opts RowsOptions) (*Result, error) {
schema, table := getSchemaAndTable(table)
sql := fmt.Sprintf(`SELECT COUNT(1) FROM "%s"."%s"`, schema, table)
result, err := client.query(statements.EstimatedTableRowCount, schema, table)
if err != nil {
return nil, err
}
// float64 to int64 conversion
estimatedRowsCount := result.Rows[0][0].(float64)
result.Rows[0] = Row{int64(estimatedRowsCount)}
return result, nil
}
func (client *Client) TableRowsCount(table string, opts RowsOptions) (*Result, error) {
// Return postgres estimated rows count on empty filter
if opts.Where == "" && client.serverType == postgresType {
res, err := client.EstimatedTableRowsCount(table, opts)
if err != nil {
return nil, err
}
n := res.Rows[0][0].(int64)
if n >= 100000 {
return res, nil
}
}
schema, tableName := getSchemaAndTable(table)
sql := fmt.Sprintf(`SELECT COUNT(1) FROM "%s"."%s"`, schema, tableName)
if opts.Where != "" {
sql += fmt.Sprintf(" WHERE %s", opts.Where)
@@ -184,12 +259,16 @@ func (client *Client) TableRowsCount(table string, opts RowsOptions) (*Result, e
}
func (client *Client) TableInfo(table string) (*Result, error) {
return client.query(statements.PG_TABLE_INFO, table)
if client.serverType == cockroachType {
return client.query(statements.TableInfoCockroach)
}
schema, table := getSchemaAndTable(table)
return client.query(statements.TableInfo, fmt.Sprintf(`"%s"."%s"`, schema, table))
}
func (client *Client) TableIndexes(table string) (*Result, error) {
schema, table := getSchemaAndTable(table)
res, err := client.query(statements.PG_TABLE_INDEXES, schema, table)
res, err := client.query(statements.TableIndexes, schema, table)
if err != nil {
return nil, err
@@ -200,7 +279,7 @@ func (client *Client) TableIndexes(table string) (*Result, error) {
func (client *Client) TableConstraints(table string) (*Result, error) {
schema, table := getSchemaAndTable(table)
res, err := client.query(statements.PG_TABLE_CONSTRAINTS, schema, table)
res, err := client.query(statements.TableConstraints, schema, table)
if err != nil {
return nil, err
@@ -211,7 +290,17 @@ func (client *Client) TableConstraints(table string) (*Result, error) {
// Returns all active queriers on the server
func (client *Client) Activity() (*Result, error) {
return client.query(statements.PG_ACTIVITY)
if client.serverType == cockroachType {
return client.query("SHOW QUERIES")
}
version := getMajorMinorVersion(client.serverVersion)
query := statements.Activity[version]
if query == "" {
query = statements.Activity["default"]
}
return client.query(query)
}
func (client *Client) Query(query string) (*Result, error) {
@@ -225,7 +314,38 @@ func (client *Client) Query(query string) (*Result, error) {
return res, err
}
func (client *Client) SetReadOnlyMode() error {
var value string
if err := client.db.Get(&value, "SHOW default_transaction_read_only;"); err != nil {
return err
}
if value == "off" {
_, err := client.db.Exec("SET default_transaction_read_only=on;")
return err
}
return nil
}
func (client *Client) ServerVersion() string {
return fmt.Sprintf("%s %s", client.serverType, client.serverVersion)
}
func (client *Client) query(query string, args ...interface{}) (*Result, error) {
// Update the last usage time
defer func() {
client.lastQueryTime = time.Now().UTC()
}()
// We're going to force-set transaction mode on every query.
// This is needed so that default mode could not be changed by user.
if command.Opts.ReadOnly {
if err := client.SetReadOnlyMode(); err != nil {
return nil, err
}
}
action := strings.ToLower(strings.Split(query, " ")[0])
if action == "update" || action == "delete" {
res, err := client.db.Exec(query, args...)
@@ -250,9 +370,11 @@ func (client *Client) query(query string, args ...interface{}) (*Result, error)
rows, err := client.db.Queryx(query, args...)
if err != nil {
if command.Opts.Debug {
log.Println("Failed query:", query, "\nArgs:", args)
}
return nil, err
}
defer rows.Close()
cols, err := rows.Columns()
@@ -308,6 +430,16 @@ func (client *Client) Close() error {
return nil
}
func (client *Client) IsIdle() bool {
mins := int(time.Since(client.lastQueryTime).Minutes())
if command.Opts.ConnectionIdleTimeout > 0 {
return mins >= command.Opts.ConnectionIdleTimeout
}
return false
}
// Fetch all rows as strings for a single column
func (client *Client) fetchRows(q string) ([]string, error) {
res, err := client.query(q)
+168 -38
View File
@@ -2,10 +2,14 @@ package client
import (
"fmt"
"log"
"os"
"os/exec"
"runtime"
"testing"
"time"
"github.com/sosedoff/pgweb/pkg/command"
"github.com/stretchr/testify/assert"
)
@@ -43,6 +47,11 @@ func getVar(name, def string) string {
}
func initVars() {
// We need to load default options to make sure all stuff works
if err := command.SetDefaultOptions(); err != nil {
log.Fatal(err)
}
serverHost = getVar("PGHOST", "localhost")
serverPort = getVar("PGPORT", "5432")
serverUser = getVar("PGUSER", "postgres")
@@ -69,6 +78,9 @@ func onWindows() bool {
}
func setup() {
// No pretty JSON for testsm
command.Opts.DisablePrettyJson = true
out, err := exec.Command(
testCommands["createdb"],
"-U", serverUser,
@@ -124,7 +136,7 @@ func teardown() {
}
}
func test_NewClientFromUrl(t *testing.T) {
func testNewClientFromUrl(t *testing.T) {
url := fmt.Sprintf("postgres://%s@%s:%s/%s?sslmode=disable", serverUser, serverHost, serverPort, serverDatabase)
client, err := NewFromUrl(url, nil)
@@ -136,7 +148,7 @@ func test_NewClientFromUrl(t *testing.T) {
assert.Equal(t, url, client.ConnectionString)
}
func test_NewClientFromUrl2(t *testing.T) {
func testNewClientFromUrl2(t *testing.T) {
url := fmt.Sprintf("postgresql://%s@%s:%s/%s?sslmode=disable", serverUser, serverHost, serverPort, serverDatabase)
client, err := NewFromUrl(url, nil)
@@ -148,18 +160,40 @@ func test_NewClientFromUrl2(t *testing.T) {
assert.Equal(t, url, client.ConnectionString)
}
func test_Test(t *testing.T) {
func testClientIdleTime(t *testing.T) {
examples := map[time.Time]bool{
time.Now(): false, // Current time
time.Now().Add(time.Minute * -30): false, // 30 minutes ago
time.Now().Add(time.Minute * -240): true, // 240 minutes ago
time.Now().Add(time.Minute * 30): false, // 30 minutes in future
time.Now().Add(time.Minute * 128): false, // 128 minutes in future
}
for ts, expected := range examples {
testClient.lastQueryTime = ts
assert.Equal(t, expected, testClient.IsIdle())
}
}
func testTest(t *testing.T) {
assert.Equal(t, nil, testClient.Test())
}
func test_Info(t *testing.T) {
func testInfo(t *testing.T) {
res, err := testClient.Info()
assert.Equal(t, nil, err)
assert.NotEqual(t, nil, res)
}
func test_Databases(t *testing.T) {
func testActivity(t *testing.T) {
res, err := testClient.Activity()
assert.Equal(t, nil, err)
assert.NotEqual(t, nil, res)
}
func testDatabases(t *testing.T) {
res, err := testClient.Databases()
assert.Equal(t, nil, err)
@@ -167,7 +201,7 @@ func test_Databases(t *testing.T) {
assert.Contains(t, res, "postgres")
}
func test_Objects(t *testing.T) {
func testObjects(t *testing.T) {
res, err := testClient.Objects()
objects := ObjectsFromResult(res)
@@ -180,6 +214,7 @@ func test_Objects(t *testing.T) {
"customers",
"daily_inventory",
"distinguished_authors",
"dummies",
"editions",
"employees",
"favorite_authors",
@@ -198,7 +233,7 @@ func test_Objects(t *testing.T) {
}
assert.Equal(t, nil, err)
assert.Equal(t, []string{"schema", "name", "type", "owner"}, res.Columns)
assert.Equal(t, []string{"schema", "name", "type", "owner", "comment"}, res.Columns)
assert.Equal(t, []string{"public"}, mapKeys(objects))
assert.Equal(t, tables, objects["public"].Tables)
assert.Equal(t, []string{"recent_shipments", "stock_view"}, objects["public"].Views)
@@ -212,7 +247,7 @@ func test_Objects(t *testing.T) {
}
}
func test_Table(t *testing.T) {
func testTable(t *testing.T) {
res, err := testClient.Table("books")
columns := []string{
@@ -222,6 +257,7 @@ func test_Table(t *testing.T) {
"character_maximum_length",
"character_set_catalog",
"column_default",
"comment",
}
assert.Equal(t, nil, err)
@@ -229,7 +265,7 @@ func test_Table(t *testing.T) {
assert.Equal(t, 4, len(res.Rows))
}
func test_TableRows(t *testing.T) {
func testTableRows(t *testing.T) {
res, err := testClient.TableRows("books", RowsOptions{})
assert.Equal(t, nil, err)
@@ -237,7 +273,7 @@ func test_TableRows(t *testing.T) {
assert.Equal(t, 15, len(res.Rows))
}
func test_TableInfo(t *testing.T) {
func testTableInfo(t *testing.T) {
res, err := testClient.TableInfo("books")
assert.Equal(t, nil, err)
@@ -245,7 +281,36 @@ func test_TableInfo(t *testing.T) {
assert.Equal(t, 1, len(res.Rows))
}
func test_TableIndexes(t *testing.T) {
func testEstimatedTableRowsCount(t *testing.T) {
var count int64 = 15
res, err := testClient.EstimatedTableRowsCount("books", RowsOptions{})
assert.Equal(t, nil, err)
assert.Equal(t, []string{"reltuples"}, res.Columns)
assert.Equal(t, []Row{Row{count}}, res.Rows)
}
func testTableRowsCount(t *testing.T) {
var count int64 = 15
res, err := testClient.TableRowsCount("books", RowsOptions{})
assert.Equal(t, nil, err)
assert.Equal(t, []string{"count"}, res.Columns)
assert.Equal(t, []Row{Row{count}}, res.Rows)
}
func testTableRowsCountWithLargeTable(t *testing.T) {
var count int64 = 100010
testClient.db.MustExec(`CREATE TABLE large_table AS SELECT s FROM generate_Series(1,100010) s;`)
testClient.db.MustExec(`VACUUM large_table;`)
res, err := testClient.TableRowsCount("large_table", RowsOptions{})
assert.Equal(t, nil, err)
assert.Equal(t, []string{"reltuples"}, res.Columns)
assert.Equal(t, []Row{Row{count}}, res.Rows)
}
func testTableIndexes(t *testing.T) {
res, err := testClient.TableIndexes("books")
assert.Equal(t, nil, err)
@@ -253,15 +318,39 @@ func test_TableIndexes(t *testing.T) {
assert.Equal(t, 2, len(res.Rows))
}
func test_TableConstraints(t *testing.T) {
func testTableConstraints(t *testing.T) {
res, err := testClient.TableConstraints("editions")
assert.Equal(t, nil, err)
assert.Equal(t, 1, len(res.Columns))
assert.Equal(t, 2, len(res.Rows))
assert.Equal(t, []string{"name", "definition"}, res.Columns)
assert.Equal(t, Row{"pkey", "PRIMARY KEY (isbn)"}, res.Rows[0])
assert.Equal(t, Row{"integrity", "CHECK (book_id IS NOT NULL AND edition IS NOT NULL)"}, res.Rows[1])
}
func test_Query(t *testing.T) {
func testTableNameWithCamelCase(t *testing.T) {
testClient.db.MustExec(`CREATE TABLE "exampleTable" (id int, name varchar);`)
testClient.db.MustExec(`INSERT INTO "exampleTable" (id, name) VALUES (1, 'foo'), (2, 'bar');`)
_, err := testClient.Table("exampleTable")
assert.NoError(t, err)
_, err = testClient.TableInfo("exampleTable")
assert.NoError(t, err)
_, err = testClient.TableConstraints("exampleTable")
assert.NoError(t, err)
_, err = testClient.TableIndexes("exampleTable")
assert.NoError(t, err)
_, err = testClient.TableRowsCount("exampleTable", RowsOptions{})
assert.NoError(t, err)
_, err = testClient.EstimatedTableRowsCount("exampleTable", RowsOptions{})
assert.NoError(t, err)
}
func testQuery(t *testing.T) {
res, err := testClient.Query("SELECT * FROM books")
assert.Equal(t, nil, err)
@@ -269,7 +358,7 @@ func test_Query(t *testing.T) {
assert.Equal(t, 15, len(res.Rows))
}
func test_QueryError(t *testing.T) {
func testQueryError(t *testing.T) {
res, err := testClient.Query("SELCT * FROM books")
assert.NotEqual(t, nil, err)
@@ -277,7 +366,7 @@ func test_QueryError(t *testing.T) {
assert.Equal(t, true, res == nil)
}
func test_QueryInvalidTable(t *testing.T) {
func testQueryInvalidTable(t *testing.T) {
res, err := testClient.Query("SELECT * FROM books2")
assert.NotEqual(t, nil, err)
@@ -285,7 +374,25 @@ func test_QueryInvalidTable(t *testing.T) {
assert.Equal(t, true, res == nil)
}
func test_ResultCsv(t *testing.T) {
func testTableRowsOrderEscape(t *testing.T) {
rows, err := testClient.TableRows("dummies", RowsOptions{SortColumn: "isDummy"})
assert.Equal(t, nil, err)
assert.Equal(t, 2, len(rows.Rows))
rows, err = testClient.TableRows("dummies", RowsOptions{SortColumn: "isdummy"})
assert.NotEqual(t, nil, err)
assert.Equal(t, `pq: column "isdummy" does not exist`, err.Error())
assert.Equal(t, true, rows == nil)
}
func testResultJSON(t *testing.T) {
result, err := testClient.Query("SELECT 'NaN'::float AS value;")
assert.NoError(t, err)
assert.Equal(t, `[{"value":null}]`, string(result.JSON()))
}
func testResultCsv(t *testing.T) {
res, _ := testClient.Query("SELECT * FROM books ORDER BY id ASC LIMIT 1")
csv := res.CSV()
@@ -294,7 +401,7 @@ func test_ResultCsv(t *testing.T) {
assert.Equal(t, expected, string(csv))
}
func test_History(t *testing.T) {
func testHistory(t *testing.T) {
_, err := testClient.Query("SELECT * FROM books WHERE id = 12345")
query := testClient.History[len(testClient.History)-1].Query
@@ -302,7 +409,7 @@ func test_History(t *testing.T) {
assert.Equal(t, "SELECT * FROM books WHERE id = 12345", query)
}
func test_HistoryError(t *testing.T) {
func testHistoryError(t *testing.T) {
_, err := testClient.Query("SELECT * FROM books123")
query := testClient.History[len(testClient.History)-1].Query
@@ -310,7 +417,7 @@ func test_HistoryError(t *testing.T) {
assert.NotEqual(t, "SELECT * FROM books123", query)
}
func test_HistoryUniqueness(t *testing.T) {
func testHistoryUniqueness(t *testing.T) {
url := fmt.Sprintf("postgres://%s@%s:%s/%s?sslmode=disable", serverUser, serverHost, serverPort, serverDatabase)
client, _ := NewFromUrl(url, nil)
@@ -321,9 +428,21 @@ func test_HistoryUniqueness(t *testing.T) {
assert.Equal(t, "SELECT * FROM books WHERE id = 1", client.History[0].Query)
}
func testReadOnlyMode(t *testing.T) {
url := fmt.Sprintf("postgres://%s@%s:%s/%s?sslmode=disable", serverUser, serverHost, serverPort, serverDatabase)
client, _ := NewFromUrl(url, nil)
err := client.SetReadOnlyMode()
assert.Equal(t, nil, err)
_, err = client.Query("CREATE TABLE foobar(id integer);")
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "in a read-only transaction")
}
func TestAll(t *testing.T) {
if onWindows() {
// Dont have access to windows machines at the moment...
t.Log("Unit testing on Windows platform is not supported.")
return
}
@@ -333,22 +452,33 @@ func TestAll(t *testing.T) {
setup()
setupClient()
test_NewClientFromUrl(t)
test_Test(t)
test_Info(t)
test_Databases(t)
test_Objects(t)
test_Table(t)
test_TableRows(t)
test_TableInfo(t)
test_TableIndexes(t)
test_TableConstraints(t)
test_Query(t)
test_QueryError(t)
test_QueryInvalidTable(t)
test_ResultCsv(t)
test_History(t)
test_HistoryError(t)
testNewClientFromUrl(t)
testClientIdleTime(t)
testTest(t)
testInfo(t)
testActivity(t)
testDatabases(t)
testObjects(t)
testTable(t)
testTableRows(t)
testTableInfo(t)
testEstimatedTableRowsCount(t)
testTableRowsCount(t)
testTableRowsCountWithLargeTable(t)
testTableIndexes(t)
testTableConstraints(t)
testTableNameWithCamelCase(t)
testQuery(t)
testQueryError(t)
testQueryInvalidTable(t)
testTableRowsOrderEscape(t)
testResultJSON(t)
testResultCsv(t)
testHistory(t)
testHistoryUniqueness(t)
testHistoryError(t)
testReadOnlyMode(t)
testDumpExport(t)
teardownClient()
teardown()
+42
View File
@@ -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
}
+46
View File
@@ -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
View File
@@ -5,9 +5,12 @@ import (
"encoding/csv"
"encoding/json"
"fmt"
"math"
"reflect"
"strconv"
"time"
"github.com/sosedoff/pgweb/pkg/command"
)
type Row []interface{}
@@ -49,6 +52,14 @@ func (res *Result) PrepareBigints() {
}
case reflect.Float64:
val := col.(float64)
// json.Marshal panics when dealing with NaN/Inf values
// issue: https://github.com/golang/go/issues/25721
if math.IsNaN(val) {
res.Rows[i][j] = nil
break
}
if val < -999999999999999 || val > 999999999999999 {
res.Rows[i][j] = strconv.FormatFloat(val, 'e', -1, 64)
}
@@ -108,7 +119,14 @@ func (res *Result) CSV() []byte {
}
func (res *Result) JSON() []byte {
data, _ := json.Marshal(res.Format())
var data []byte
if command.Opts.DisablePrettyJson {
data, _ = json.Marshal(res.Format())
} else {
data, _ = json.MarshalIndent(res.Format(), "", " ")
}
return data
}
+35 -5
View File
@@ -10,6 +10,7 @@ import (
"os"
"strings"
"sync"
"time"
"golang.org/x/crypto/ssh"
@@ -18,10 +19,11 @@ import (
)
const (
PORT_START = 29168
PORT_LIMIT = 500
portStart = 29168
portLimit = 500
)
// Tunnel represents the connection between local and remote server
type Tunnel struct {
TargetHost string
TargetPort string
@@ -36,6 +38,14 @@ func privateKeyPath() string {
return os.Getenv("HOME") + "/.ssh/id_rsa"
}
func expandKeyPath(path string) string {
home := os.Getenv("HOME")
if home == "" {
return path
}
return strings.Replace(path, "~", home, 1)
}
func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
@@ -53,7 +63,14 @@ func parsePrivateKey(keyPath string) (ssh.Signer, error) {
func makeConfig(info *shared.SSHInfo) (*ssh.ClientConfig, error) {
methods := []ssh.AuthMethod{}
keyPath := privateKeyPath()
// Try to use user-provided key, fallback to system default key
keyPath := info.Key
if keyPath == "" {
keyPath = privateKeyPath()
} else {
keyPath = expandKeyPath(keyPath)
}
if fileExists(keyPath) {
key, err := parsePrivateKey(keyPath)
if err != nil {
@@ -65,7 +82,16 @@ func makeConfig(info *shared.SSHInfo) (*ssh.ClientConfig, error) {
methods = append(methods, ssh.Password(info.Password))
return &ssh.ClientConfig{User: info.User, Auth: methods}, nil
cfg := &ssh.ClientConfig{
User: info.User,
Auth: methods,
Timeout: time.Second * 10,
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
}
return cfg, nil
}
func (tunnel *Tunnel) sshEndpoint() string {
@@ -99,6 +125,7 @@ func (tunnel *Tunnel) handleConnection(local net.Conn) {
local.Close()
}
// Close closes the tunnel connection
func (tunnel *Tunnel) Close() {
if tunnel.Client != nil {
tunnel.Client.Close()
@@ -109,6 +136,7 @@ func (tunnel *Tunnel) Close() {
}
}
// Configure establishes the tunnel between localhost and remote machine
func (tunnel *Tunnel) Configure() error {
config, err := makeConfig(tunnel.SSHInfo)
if err != nil {
@@ -131,6 +159,7 @@ func (tunnel *Tunnel) Configure() error {
return nil
}
// Start starts the connection handler loop
func (tunnel *Tunnel) Start() {
defer tunnel.Close()
@@ -144,13 +173,14 @@ func (tunnel *Tunnel) Start() {
}
}
// NewTunnel instantiates a new tunnel struct from given ssh info
func NewTunnel(sshInfo *shared.SSHInfo, dbUrl string) (*Tunnel, error) {
uri, err := url.Parse(dbUrl)
if err != nil {
return nil, err
}
listenPort, err := connection.AvailablePort(PORT_START, PORT_LIMIT)
listenPort, err := connection.FindAvailablePort(portStart, portLimit)
if err != nil {
return nil, err
}
+15
View File
@@ -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
View File
@@ -1,50 +1,136 @@
package command
import (
"errors"
"os"
"os/user"
"strings"
"github.com/jessevdk/go-flags"
)
type Options struct {
Version bool `short:"v" long:"version" description:"Print version"`
Debug bool `short:"d" long:"debug" description:"Enable debugging mode" default:"false"`
Url string `long:"url" description:"Database connection string"`
Host string `long:"host" description:"Server hostname or IP"`
Port int `long:"port" description:"Server port" default:"5432"`
User string `long:"user" description:"Database user"`
Pass string `long:"pass" description:"Password for user"`
DbName string `long:"db" description:"Database name"`
Ssl string `long:"ssl" description:"SSL option"`
HttpHost string `long:"bind" description:"HTTP server host" default:"localhost"`
HttpPort uint `long:"listen" description:"HTTP server listen port" default:"8081"`
AuthUser string `long:"auth-user" description:"HTTP basic auth user"`
AuthPass string `long:"auth-pass" description:"HTTP basic auth password"`
SkipOpen bool `short:"s" long:"skip-open" description:"Skip browser open on start"`
Sessions bool `long:"sessions" description:"Enable multiple database sessions" default:"false"`
Prefix string `long:"prefix" description:"Add a url prefix"`
Version bool `short:"v" long:"version" description:"Print version"`
Debug bool `short:"d" long:"debug" description:"Enable debugging mode"`
Url string `long:"url" description:"Database connection string"`
Host string `long:"host" description:"Server hostname or IP" default:"localhost"`
Port int `long:"port" description:"Server port" default:"5432"`
User string `long:"user" description:"Database user"`
Pass string `long:"pass" description:"Password for user"`
DbName string `long:"db" description:"Database name"`
Ssl string `long:"ssl" description:"SSL option"`
HttpHost string `long:"bind" description:"HTTP server host" default:"localhost"`
HttpPort uint `long:"listen" description:"HTTP server listen port" default:"8081"`
AuthUser string `long:"auth-user" description:"HTTP basic auth user"`
AuthPass string `long:"auth-pass" description:"HTTP basic auth password"`
SkipOpen bool `short:"s" long:"skip-open" description:"Skip browser open on start"`
Sessions bool `long:"sessions" description:"Enable multiple database sessions"`
Prefix string `long:"prefix" description:"Add a url prefix"`
ReadOnly bool `long:"readonly" description:"Run database connection in readonly mode"`
LockSession bool `long:"lock-session" description:"Lock session to a single database connection"`
Bookmark string `short:"b" long:"bookmark" description:"Bookmark to use for connection. Bookmark files are stored under $HOME/.pgweb/bookmarks/*.toml" default:""`
BookmarksDir string `long:"bookmarks-dir" description:"Overrides default directory for bookmark files to search" default:""`
DisablePrettyJson bool `long:"no-pretty-json" description:"Disable JSON formatting feature for result export"`
DisableSSH bool `long:"no-ssh" description:"Disable database connections via SSH"`
ConnectBackend string `long:"connect-backend" description:"Enable database authentication through a third party backend"`
ConnectToken string `long:"connect-token" description:"Authentication token for the third-party connect backend"`
ConnectHeaders string `long:"connect-headers" description:"List of headers to pass to the connect backend"`
DisableConnectionIdleTimeout bool `long:"no-idle-timeout" description:"Disable connection idle timeout"`
ConnectionIdleTimeout int `long:"idle-timeout" description:"Set connection idle timeout in minutes" default:"180"`
Cors bool `long:"cors" description:"Enable Cross-Origin Resource Sharing (CORS)"`
CorsOrigin string `long:"cors-origin" description:"Allowed CORS origins" default:"*"`
}
var Opts Options
func ParseOptions() error {
_, err := flags.ParseArgs(&Opts, os.Args)
func ParseOptions(args []string) (Options, error) {
var opts = Options{}
_, err := flags.ParseArgs(&opts, args)
if err != nil {
return err
return opts, err
}
if Opts.Url == "" {
Opts.Url = os.Getenv("DATABASE_URL")
if opts.Url == "" {
opts.Url = os.Getenv("DATABASE_URL")
}
if opts.Prefix == "" {
opts.Prefix = os.Getenv("URL_PREFIX")
}
// Handle edge case where pgweb is started with a default host `localhost` and no user.
// When user is not set the `lib/pq` connection will fail and cause pgweb's termination.
if (opts.Host == "localhost" || opts.Host == "127.0.0.1") && opts.User == "" {
if username := GetCurrentUser(); username != "" {
opts.User = username
} else {
opts.Host = ""
}
}
if os.Getenv("SESSIONS") != "" {
Opts.Sessions = true
opts.Sessions = true
}
if Opts.Prefix != "" && !strings.Contains(Opts.Prefix, "/") {
Opts.Prefix = Opts.Prefix + "/"
if os.Getenv("LOCK_SESSION") != "" {
opts.LockSession = true
opts.Sessions = false
}
if opts.Sessions || opts.ConnectBackend != "" {
opts.Bookmark = ""
opts.Url = ""
opts.Host = ""
opts.User = ""
opts.Pass = ""
opts.DbName = ""
opts.Ssl = ""
}
if opts.Prefix != "" && !strings.Contains(opts.Prefix, "/") {
opts.Prefix = opts.Prefix + "/"
}
if opts.AuthUser == "" && os.Getenv("AUTH_USER") != "" {
opts.AuthUser = os.Getenv("AUTH_USER")
}
if opts.AuthPass == "" && os.Getenv("AUTH_PASS") != "" {
opts.AuthPass = os.Getenv("AUTH_PASS")
}
if opts.ConnectBackend != "" {
if !opts.Sessions {
return opts, errors.New("--sessions flag must be set")
}
if opts.ConnectToken == "" {
return opts, errors.New("--connect-token flag must be set")
}
} else {
if opts.ConnectToken != "" || opts.ConnectHeaders != "" {
return opts, errors.New("--connect-backend flag must be set")
}
}
return opts, nil
}
// SetDefaultOptions parses and assigns the options
func SetDefaultOptions() error {
opts, err := ParseOptions([]string{})
if err != nil {
return err
}
Opts = opts
return nil
}
// GetCurrentUser returns a current user name
func GetCurrentUser() string {
u, _ := user.Current()
if u != nil {
return u.Username
}
return os.Getenv("USER")
}
+32 -23
View File
@@ -1,38 +1,47 @@
package command
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_Options(t *testing.T) {
err := ParseOptions()
func TestParseOptions(t *testing.T) {
// Test default behavior
opts, err := ParseOptions([]string{})
assert.NoError(t, err)
assert.Equal(t, false, Opts.Sessions)
assert.Equal(t, "", Opts.Prefix)
}
assert.Equal(t, false, opts.Sessions)
assert.Equal(t, "", opts.Prefix)
assert.Equal(t, "", opts.ConnectToken)
assert.Equal(t, "", opts.ConnectHeaders)
assert.Equal(t, false, opts.DisableSSH)
assert.Equal(t, false, opts.DisablePrettyJson)
assert.Equal(t, false, opts.DisableConnectionIdleTimeout)
assert.Equal(t, 180, opts.ConnectionIdleTimeout)
assert.Equal(t, false, opts.Cors)
assert.Equal(t, "*", opts.CorsOrigin)
func Test_SessionsOption(t *testing.T) {
oldargs := os.Args
defer func() { os.Args = oldargs }()
// Test sessions
opts, err = ParseOptions([]string{"--sessions", "1"})
assert.NoError(t, err)
assert.Equal(t, true, opts.Sessions)
os.Args = []string{"--sessions", "1"}
assert.NoError(t, ParseOptions())
assert.Equal(t, true, Opts.Sessions)
}
// Test url prefix
opts, err = ParseOptions([]string{"--prefix", "pgweb"})
assert.NoError(t, err)
assert.Equal(t, "pgweb/", opts.Prefix)
func Test_PrefixOption(t *testing.T) {
oldargs := os.Args
defer func() { os.Args = oldargs }()
opts, err = ParseOptions([]string{"--prefix", "pgweb/"})
assert.NoError(t, err)
assert.Equal(t, "pgweb/", opts.Prefix)
os.Args = []string{"--prefix", "pgweb"}
assert.NoError(t, ParseOptions())
assert.Equal(t, "pgweb/", Opts.Prefix)
// Test connect backend options
opts, err = ParseOptions([]string{"--connect-backend", "test"})
assert.EqualError(t, err, "--sessions flag must be set")
os.Args = []string{"--prefix", "pgweb/"}
assert.NoError(t, ParseOptions())
assert.Equal(t, "pgweb/", Opts.Prefix)
opts, err = ParseOptions([]string{"--connect-backend", "test", "--sessions"})
assert.EqualError(t, err, "--connect-token flag must be set")
opts, err = ParseOptions([]string{"--connect-backend", "test", "--sessions", "--connect-token", "token"})
assert.NoError(t, err)
}
+3 -1
View File
@@ -1,6 +1,8 @@
package command
const VERSION = "0.9.3"
const (
Version = "0.11.2"
)
var (
GitCommit string
+68 -41
View File
@@ -11,6 +11,13 @@ import (
"github.com/sosedoff/pgweb/pkg/command"
)
// Common errors
var (
errCantDetectUser = errors.New("Could not detect default username")
errInvalidURLFormat = errors.New("Invalid URL. Valid format: postgres://user:password@host:port/db?sslmode=mode")
)
// currentUser returns a current user name
func currentUser() (string, error) {
u, err := user.Current()
if err == nil {
@@ -22,44 +29,73 @@ func currentUser() (string, error) {
return name, nil
}
return "", errors.New("Unable to detect OS user")
return "", errCantDetectUser
}
func FormatUrl(opts command.Options) (string, error) {
// Check if connection url has a correct postgres prefix
func hasValidPrefix(str string) bool {
return strings.HasPrefix(str, "postgres://") || strings.HasPrefix(str, "postgresql://")
}
// Extract all query vals and return as a map
func valsFromQuery(vals neturl.Values) map[string]string {
result := map[string]string{}
for k, v := range vals {
result[strings.ToLower(k)] = v[0]
}
return result
}
// FormatURL reformats the existing connection string
func FormatURL(opts command.Options) (string, error) {
url := opts.Url
// Make sure to only accept urls in a standard format
if !strings.HasPrefix(url, "postgres://") && !strings.HasPrefix(url, "postgresql://") {
return "", errors.New("Invalid URL. Valid format: postgres://user:password@host:port/db?sslmode=mode")
// Validate connection string prefix
if !hasValidPrefix(url) {
return "", errInvalidURLFormat
}
// Special handling for local connections
if strings.Contains(url, "localhost") || strings.Contains(url, "127.0.0.1") {
if !strings.Contains(url, "?sslmode") {
if opts.Ssl == "" {
url += fmt.Sprintf("?sslmode=%s", "disable")
} else {
url += fmt.Sprintf("?sslmode=%s", opts.Ssl)
// Validate the URL
uri, err := neturl.Parse(url)
if err != nil {
return "", errInvalidURLFormat
}
// Get query params
params := valsFromQuery(uri.Query())
// Determine if we need to specify sslmode if it's missing
if params["sslmode"] == "" {
if opts.Ssl == "" {
// Only modify sslmode for local connections
if strings.Contains(uri.Host, "localhost") || strings.Contains(uri.Host, "127.0.0.1") {
params["sslmode"] = "disable"
}
} else {
params["sslmode"] = opts.Ssl
}
}
// Append sslmode parameter only if its defined as a flag and not present
// in the connection string.
if !strings.Contains(url, "?sslmode") && opts.Ssl != "" {
url += fmt.Sprintf("?sslmode=%s", opts.Ssl)
// Rebuild query params
query := neturl.Values{}
for k, v := range params {
query.Add(k, v)
}
uri.RawQuery = query.Encode()
return url, nil
return uri.String(), nil
}
// IsBlank returns true if command options do not contain connection details
func IsBlank(opts command.Options) bool {
return opts.Host == "" && opts.User == "" && opts.DbName == "" && opts.Url == ""
}
func BuildString(opts command.Options) (string, error) {
// BuildStringFromOptions returns a new connection string built from options
func BuildStringFromOptions(opts command.Options) (string, error) {
// If connection string is provided we just use that
if opts.Url != "" {
return FormatUrl(opts)
return FormatURL(opts)
}
// Try to detect user from current OS user
@@ -71,31 +107,22 @@ func BuildString(opts command.Options) (string, error) {
}
// Disable ssl for localhost connections, most users have it disabled
if opts.Host == "localhost" || opts.Host == "127.0.0.1" {
if opts.Ssl == "" {
opts.Ssl = "disable"
}
}
url := "postgres://"
if opts.User != "" {
url += opts.User
}
if opts.Pass != "" {
url += fmt.Sprintf(":%s", neturl.QueryEscape(opts.Pass))
}
url += fmt.Sprintf("@%s:%d", opts.Host, opts.Port)
if opts.DbName != "" {
url += fmt.Sprintf("/%s", opts.DbName)
if opts.Ssl == "" && (opts.Host == "localhost" || opts.Host == "127.0.0.1") {
opts.Ssl = "disable"
}
query := neturl.Values{}
if opts.Ssl != "" {
url += fmt.Sprintf("?sslmode=%s", opts.Ssl)
query.Add("sslmode", opts.Ssl)
}
return url, nil
url := neturl.URL{
Scheme: "postgres",
Host: fmt.Sprintf("%v:%v", opts.Host, opts.Port),
User: neturl.UserPassword(opts.User, opts.Pass),
Path: fmt.Sprintf("/%s", opts.DbName),
RawQuery: query.Encode(),
}
return url.String(), nil
}
+20 -23
View File
@@ -2,6 +2,7 @@ package connection
import (
"fmt"
"net/url"
"os/user"
"testing"
@@ -13,12 +14,13 @@ func Test_Invalid_Url(t *testing.T) {
opts := command.Options{}
examples := []string{
"postgre://foobar",
"tcp://blah",
"foobar",
}
for _, val := range examples {
opts.Url = val
str, err := BuildString(opts)
str, err := BuildStringFromOptions(opts)
assert.Equal(t, "", str)
assert.Error(t, err)
@@ -28,14 +30,14 @@ func Test_Invalid_Url(t *testing.T) {
func Test_Valid_Url(t *testing.T) {
url := "postgres://myhost/database"
str, err := BuildString(command.Options{Url: url})
str, err := BuildStringFromOptions(command.Options{Url: url})
assert.Equal(t, nil, err)
assert.Equal(t, url, str)
}
func Test_Url_And_Ssl_Flag(t *testing.T) {
str, err := BuildString(command.Options{
str, err := BuildStringFromOptions(command.Options{
Url: "postgres://myhost/database",
Ssl: "disable",
})
@@ -45,57 +47,51 @@ func Test_Url_And_Ssl_Flag(t *testing.T) {
}
func Test_Localhost_Url_And_No_Ssl_Flag(t *testing.T) {
str, err := BuildString(command.Options{
str, err := BuildStringFromOptions(command.Options{
Url: "postgres://localhost/database",
})
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://localhost/database?sslmode=disable", str)
str, err = BuildString(command.Options{
str, err = BuildStringFromOptions(command.Options{
Url: "postgres://127.0.0.1/database",
})
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://127.0.0.1/database?sslmode=disable", str)
}
func Test_Localhost_Url_And_Ssl_Flag(t *testing.T) {
str, err := BuildString(command.Options{
str, err := BuildStringFromOptions(command.Options{
Url: "postgres://localhost/database",
Ssl: "require",
})
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://localhost/database?sslmode=require", str)
str, err = BuildString(command.Options{
str, err = BuildStringFromOptions(command.Options{
Url: "postgres://127.0.0.1/database",
Ssl: "require",
})
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://127.0.0.1/database?sslmode=require", str)
}
func Test_Localhost_Url_And_Ssl_Arg(t *testing.T) {
str, err := BuildString(command.Options{
str, err := BuildStringFromOptions(command.Options{
Url: "postgres://localhost/database?sslmode=require",
})
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://localhost/database?sslmode=require", str)
str, err = BuildString(command.Options{
str, err = BuildStringFromOptions(command.Options{
Url: "postgres://127.0.0.1/database?sslmode=require",
})
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://127.0.0.1/database?sslmode=require", str)
}
func Test_Flag_Args(t *testing.T) {
str, err := BuildString(command.Options{
str, err := BuildStringFromOptions(command.Options{
Host: "host",
Port: 5432,
User: "user",
@@ -116,12 +112,12 @@ func Test_Localhost(t *testing.T) {
DbName: "db",
}
str, err := BuildString(opts)
str, err := BuildStringFromOptions(opts)
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://user:password@localhost:5432/db?sslmode=disable", str)
opts.Host = "127.0.0.1"
str, err = BuildString(opts)
str, err = BuildStringFromOptions(opts)
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://user:password@127.0.0.1:5432/db?sslmode=disable", str)
}
@@ -136,7 +132,7 @@ func Test_Localhost_And_Ssl(t *testing.T) {
Ssl: "require",
}
str, err := BuildString(opts)
str, err := BuildStringFromOptions(opts)
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://user:password@localhost:5432/db?sslmode=require", str)
}
@@ -144,18 +140,19 @@ func Test_Localhost_And_Ssl(t *testing.T) {
func Test_No_User(t *testing.T) {
opts := command.Options{Host: "host", Port: 5432, DbName: "db"}
u, _ := user.Current()
str, err := BuildString(opts)
str, err := BuildStringFromOptions(opts)
userAndPass := url.UserPassword(u.Username, "").String()
assert.Equal(t, nil, err)
assert.Equal(t, fmt.Sprintf("postgres://%s@host:5432/db", u.Username), str)
assert.Equal(t, fmt.Sprintf("postgres://%s@host:5432/db", userAndPass), str)
}
func Test_Port(t *testing.T) {
opts := command.Options{Host: "host", User: "user", Port: 5000, DbName: "db"}
str, err := BuildString(opts)
str, err := BuildStringFromOptions(opts)
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://user@host:5000/db", str)
assert.Equal(t, "postgres://user:@host:5000/db", str)
}
func Test_Blank(t *testing.T) {
+5 -6
View File
@@ -7,10 +7,9 @@ import (
"strings"
)
// Check if the TCP port available on localhost
func portAvailable(port int) bool {
// IsPortAvailable returns true if there's no listeners on a given port
func IsPortAvailable(port int) bool {
conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%v", port))
if err != nil {
if strings.Index(err.Error(), "connection refused") > 0 {
return true
@@ -22,10 +21,10 @@ func portAvailable(port int) bool {
return false
}
// Get available TCP port on localhost by trying available ports in a range
func AvailablePort(start int, limit int) (int, error) {
// FindAvailablePort returns the first available TCP port in the range
func FindAvailablePort(start int, limit int) (int, error) {
for i := start; i <= (start + limit); i++ {
if portAvailable(i) {
if IsPortAvailable(i) {
return i, nil
}
}
+12 -12
View File
@@ -10,14 +10,14 @@ import (
"github.com/stretchr/testify/assert"
)
func Test_portAvailable(t *testing.T) {
func TestIsPortAvailable(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("FIXME")
}
assert.Equal(t, true, portAvailable(8081))
assert.Equal(t, true, IsPortAvailable(30000))
serv, err := net.Listen("tcp", "127.0.0.1:8081")
serv, err := net.Listen("tcp", "127.0.0.1:30000")
if err != nil {
fmt.Fprintln(os.Stderr, "Unable to start test tcp listener:", err)
t.Fail()
@@ -35,20 +35,20 @@ func Test_portAvailable(t *testing.T) {
}
}()
assert.Equal(t, false, portAvailable(8081))
assert.Equal(t, true, portAvailable(8082))
assert.Equal(t, false, IsPortAvailable(30000))
assert.Equal(t, true, IsPortAvailable(30001))
}
func Test_getAvailablePort(t *testing.T) {
func TestFindAvailablePort(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("FIXME")
}
port, err := AvailablePort(8081, 1)
port, err := FindAvailablePort(30000, 1)
assert.Equal(t, nil, err)
assert.Equal(t, 8081, port)
assert.Equal(t, 30000, port)
serv, err := net.Listen("tcp", "127.0.0.1:8081")
serv, err := net.Listen("tcp", "127.0.0.1:30000")
if err != nil {
fmt.Fprintln(os.Stderr, "Unable to start test tcp listener:", err)
t.Fail()
@@ -65,11 +65,11 @@ func Test_getAvailablePort(t *testing.T) {
}
}()
port, err = AvailablePort(8081, 0)
port, err = FindAvailablePort(30000, 0)
assert.EqualError(t, err, "No available port")
assert.Equal(t, -1, port)
port, err = AvailablePort(8081, 1)
port, err = FindAvailablePort(30000, 1)
assert.Equal(t, nil, err)
assert.Equal(t, 8082, port)
assert.Equal(t, 30001, port)
}
+131 -64
View File
File diff suppressed because one or more lines are too long
+48 -35
View File
@@ -1,9 +1,7 @@
package statements
const (
// ---------------------------------------------------------------------------
PG_DATABASES = `
Databases = `
SELECT
datname
FROM
@@ -15,7 +13,7 @@ ORDER BY
// ---------------------------------------------------------------------------
PG_SCHEMAS = `
Schemas = `
SELECT
schema_name
FROM
@@ -25,7 +23,7 @@ ORDER BY
// ---------------------------------------------------------------------------
PG_INFO = `
Info = `
SELECT
session_user,
current_user,
@@ -35,11 +33,22 @@ SELECT
inet_client_port(),
inet_server_addr(),
inet_server_port(),
version()`
version()`
// ---------------------------------------------------------------------------
PG_TABLE_INDEXES = `
EstimatedTableRowCount = `
SELECT
reltuples
FROM
pg_class
WHERE
oid = ('"' || $1::text || '"."' || $2::text || '"')::regclass
`
// ---------------------------------------------------------------------------
TableIndexes = `
SELECT
indexname, indexdef
FROM
@@ -50,9 +59,10 @@ WHERE
// ---------------------------------------------------------------------------
PG_TABLE_CONSTRAINTS = `
TableConstraints = `
SELECT
pg_get_constraintdef(c.oid, true) as condef
conname as name,
pg_get_constraintdef(c.oid, true) as definition
FROM
pg_constraint c
JOIN
@@ -67,23 +77,31 @@ ORDER BY
// ---------------------------------------------------------------------------
PG_TABLE_INFO = `
TableInfo = `
SELECT
pg_size_pretty(pg_table_size($1)) AS data_size,
pg_size_pretty(pg_indexes_size($1)) AS index_size,
pg_size_pretty(pg_total_relation_size($1)) AS total_size,
(SELECT reltuples FROM pg_class WHERE oid = $1::regclass) AS rows_count`
TableInfoCockroach = `
SELECT
'n/a' AS data_size,
'n/a' AS index_size,
'n/a' AS total_size,
'n/a' AS rows_count`
// ---------------------------------------------------------------------------
PG_TABLE_SCHEMA = `
TableSchema = `
SELECT
column_name,
data_type,
is_nullable,
character_maximum_length,
character_set_catalog,
column_default
column_default,
pg_catalog.col_description(('"' || $1::text || '"."' || $2::text || '"')::regclass::oid, ordinal_position) as comment
FROM
information_schema.columns
WHERE
@@ -92,7 +110,7 @@ WHERE
// ---------------------------------------------------------------------------
PG_MATERIALIZED_VIEW_SCHEMA = `
MaterializedView = `
SELECT
attname as column_name,
atttypid::regtype AS data_type,
@@ -109,26 +127,7 @@ WHERE
// ---------------------------------------------------------------------------
PG_ACTIVITY = `
SELECT
datname,
query,
state,
waiting,
query_start,
state_change,
pid,
datid,
application_name,
client_addr
FROM
pg_stat_activity
WHERE
state IS NOT NULL`
// ---------------------------------------------------------------------------
PG_OBJECTS = `
Objects = `
SELECT
n.nspname as "schema",
c.relname as "name",
@@ -141,7 +140,8 @@ SELECT
WHEN 's' THEN 'special'
WHEN 'f' THEN 'foreign_table'
END as "type",
pg_catalog.pg_get_userbyid(c.relowner) as "owner"
pg_catalog.pg_get_userbyid(c.relowner) as "owner",
pg_catalog.obj_description(c.oid) as "comment"
FROM
pg_catalog.pg_class c
LEFT JOIN
@@ -149,6 +149,19 @@ LEFT JOIN
WHERE
c.relkind IN ('r','v','m','S','s','') AND
n.nspname !~ '^pg_toast' AND
n.nspname NOT IN ('information_schema', 'pg_catalog')
n.nspname NOT IN ('information_schema', 'pg_catalog') AND
has_schema_privilege(n.nspname, 'USAGE')
ORDER BY 1, 2`
)
var (
Activity = map[string]string{
"default": "SELECT * FROM pg_stat_activity WHERE datname = current_database()",
"9.1": "SELECT datname, current_query, waiting, query_start, procpid as pid, datid, application_name, client_addr FROM pg_stat_activity WHERE datname = current_database()",
"9.2": "SELECT datname, query, state, waiting, query_start, state_change, pid, datid, application_name, client_addr FROM pg_stat_activity WHERE datname = current_database()",
"9.3": "SELECT datname, query, state, waiting, query_start, state_change, pid, datid, application_name, client_addr FROM pg_stat_activity WHERE datname = current_database()",
"9.4": "SELECT datname, query, state, waiting, query_start, state_change, pid, datid, application_name, client_addr FROM pg_stat_activity WHERE datname = current_database()",
"9.5": "SELECT datname, query, state, waiting, query_start, state_change, pid, datid, application_name, client_addr FROM pg_stat_activity WHERE datname = current_database()",
"9.6": "SELECT datname, query, state, wait_event, wait_event_type, query_start, state_change, pid, datid, application_name, client_addr FROM pg_stat_activity WHERE datname = current_database()",
}
)
+10
View File
@@ -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
+15
View File
@@ -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
View File
@@ -1,20 +1,34 @@
#!/bin/bash
#
# Integartion testing with dockerized Postgres servers
#
# Requires Docker for Mac to run on OSX.
# Install: https://docs.docker.com/engine/installation/mac/
#
set -e
export PGHOST=${PGHOST:-192.168.99.100}
export PGHOST=${PGHOST:-localhost}
export PGUSER="postgres"
export PGPASSWORD=""
export PGDATABASE="booktown"
export PGPORT="15432"
for i in {1..6}
# TODO: Enable the 10.x branch when it's supported on Travis.
# Local 10.x version is required so that pg_dump can properly work with older versions.
# 10.x branch is normally supported.
versions="9.1 9.2 9.3 9.4 9.5 9.6"
for i in $versions
do
export PGVERSION="9.$i"
export PGVERSION="$i"
echo "------------------------------- BEGIN TEST -------------------------------"
echo "Running tests against PostgreSQL v$PGVERSION"
docker rm -f postgres || true
docker run -p $PGPORT:5432 --name postgres -e POSTGRES_PASSWORD=$PGPASSWORD -d postgres:$PGVERSION
sleep 5
make test
echo "----------"
done
echo "-------------------------------- END TEST --------------------------------"
done
+58
View File
@@ -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
View File
@@ -81,18 +81,62 @@
line-height: 33px;
height: 33px;
padding: 0px 8px;
padding-left: 12px;
color: #555;
font-weight: bold;
font-size: 13px;
}
#sidebar .title.main {
line-height: 50px;
height: 50px;
line-height: 50px;
font-size: 14px;
background: #79589f;
color: #fff;
font-size: 14px;
padding: 0px;
}
#sidebar .title.main i.fa-database {
position: absolute;
left: 8px;
top: 18px;
}
#sidebar span.current-database,
#sidebar input.typeahead {
margin-left: 30px;
display: inline-block;
width: 200px;
height: 50px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#sidebar input.typeahead {
display: none;
background: #79589f;
width: 200px;
height: 22px;
color: #fff;
font-weight: normal;
outline: none;
border: 0px none;
}
#sidebar input.typeahead::-webkit-input-placeholder {
color: #d6cce2;
}
#sidebar ul.typeahead {
margin-left: 30px;
overflow-y: auto;
max-height: 300px;
z-index: 999;
}
#sidebar ul.typeahead a {
font-size: 13px;
padding: 3px 10px;
}
#sidebar div.tables-list {
@@ -110,7 +154,7 @@
}
#sidebar div.tables-list .title span.current-database {
margin-left: 4px;
cursor: pointer;
}
#sidebar div.tables-list .title:hover span.refresh {
@@ -118,8 +162,9 @@
}
#sidebar div.tables-list .title span.refresh {
float: right;
margin-right: 4px;
position: absolute;
top: 0px;
right: 6px;
cursor: pointer;
color: #fff;
display: none;
@@ -195,6 +240,16 @@
border-bottom: solid 1px #ddd;
}
#input .actions #result-rows-count {
font-size: 12px;
display: inline-block;
height: 30px;
line-height: 30px;
float: left;
margin-right: 10px;
color: #999;
}
#input .actions .btn {
line-height: 30px;
height: 30px;
@@ -377,11 +432,21 @@
line-height: 24px;
color: #333;
font-weight: bold;
cursor: pointer;
-webkit-font-smoothing: antialiased;
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
user-select: none;
}
#results th:hover {
background: #f8f8f8;
}
#results th.active {
background: #f3f3f3;
background: #f3faff;
}
#results[data-mode="browse"] th:hover {
@@ -475,6 +540,7 @@
position: fixed;
right: 8px;
top: 10px;
display: none;
}
#edit_connection, #close_connection {
@@ -505,14 +571,32 @@
margin-top: 50px;
}
.connection-settings h1 {
.connection-settings .header {
margin-bottom: 25px;
}
.connection-settings .header h1 {
text-align: center;
text-shadow: 0px 1px 0px #fff;
margin-bottom: 25px;
color: #999;
font-weight: normal;
}
.connection-settings .header .version {
font-size: 12px;
color: #aaa;
text-align: center;
display: block;
}
.connection-settings .header .update {
font-size: 12px;
text-align: center;
padding: 4px;
margin: 12px 0px;
display: none;
}
.connection-settings form {
background: #f6f6f6;
padding: 25px;
@@ -525,6 +609,10 @@
font-size: 12px;
}
.connection-settings form .no-left-padding {
padding-left: 0;
}
.connection-scheme-group {
display: none;
}
@@ -573,6 +661,14 @@
line-height: 30px;
height: 30px;
padding: 0px 8px;
overflow: hidden;
}
.schema .schema-container .schema-group .schema-group-count {
color: #999;
display: inline-block;
float: right;
margin-right: 8px;
}
.schema .schema-container .schema-group ul {
@@ -635,4 +731,4 @@
.ace_active-line {
background: none !important;
}
}
+71 -22
View File
@@ -13,7 +13,10 @@
<script type="text/javascript" src="static/js/ace.js"></script>
<script type="text/javascript" src="static/js/ace-pgsql.js"></script>
<script type="text/javascript" src="static/js/bootstrap-contextmenu.js"></script>
<script type="text/javascript" src="static/js/app.js"></script>
<script type="text/javascript" src="static/js/utils.js"></script>
<script type="text/javascript" src="static/js/bootstrap3-typeahead.min.js"></script>
<script type="text/javascript" src="static/js/app.js"></script>
<script type="text/javascript" src="static/js/base64.js"></script>
</head>
<body>
<div id="main">
@@ -23,7 +26,7 @@
<li id="table_structure">Structure</li>
<li id="table_indexes">Indexes</li>
<li id="table_constraints">Constraints</li>
<li id="table_query" class="selected">SQL Query</li>
<li id="table_query" class="selected">Query</li>
<li id="table_history">History</li>
<li id="table_activity">Activity</li>
<li id="table_connection">Connection</li>
@@ -37,8 +40,9 @@
<div id="sidebar">
<div class="tables-list">
<div class="wrap">
<div class="title main">
<div class="title main">
<i class="fa fa-database"></i> <span class="current-database" id="current_database"></span>
<input class="typeahead current_database" id="database_search" type="text" placeholder="Search database">
<span class="refresh" id="refresh_tables" title="Refresh tables list"><i class="fa fa-refresh"></i></span>
</div>
<div id="objects"></div>
@@ -65,6 +69,7 @@
<input type="button" id="explain" value="Explain Query" class="btn btn-sm btn-default" />
<div id="query_progress">Please wait, query is executing...</div>
<div class="pull-right">
<span id="result-rows-count"></span>
<input type="button" id="json" value="JSON" class="btn btn-sm btn-default" />
<input type="button" id="csv" value="CSV" class="btn btn-sm btn-default" />
<input type="button" id="xml" value="XML" class="btn btn-sm btn-default" />
@@ -74,7 +79,10 @@
</div>
<div id="output">
<div class="wrapper">
<table id="results" class="table"></table>
<table id="results" class="table">
<thead id="results_header"></thead>
<tbody id="results_body"></tbody>
</table>
</div>
</div>
<div id="pagination">
@@ -94,7 +102,7 @@
<option value="null">IS NULL</option>
<option value="not_null">NOT NULL</option>
</select>
<input type="text" class="form-control" placeholder="Search query" />
<input type="text" class="form-control" placeholder="Filter value" id="table_filter_value" />
<button class="btn btn-primary btn-sm apply-filters" type="submit">Apply</button>
<button class="btn btn-default btn-sm reset-filters"><i class="fa fa-times"></i></button>
</form>
@@ -112,7 +120,11 @@
<div id="connection_window">
<div class="connection-settings">
<h1>pgweb</h1>
<div class="header">
<h1>pgweb</h1>
<div class="version"></div>
<div class="update alert alert-warning"></div>
</div>
<form role="form" class="form-horizontal" id="connection_form">
<div class="text-center">
@@ -129,8 +141,11 @@
<div class="form-group">
<div class="col-sm-12">
<label>Enter server URL scheme</label>
<input type="text" class="form-control" id="connection_url" name="url">
<p class="help-block">URL format: postgres://user:password@host:port/db?sslmode=mode</p>
<input type="text" class="form-control" id="connection_url" name="url" autocomplete="off">
<p class="help-block">
URL format: postgres://user:password@host:port/db?sslmode=mode<br/>
Read more on PostgreSQL <a href="https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING" target="_blank">connection string format</a>.
</p>
</div>
</div>
</div>
@@ -145,9 +160,12 @@
<div class="form-group">
<label class="col-sm-3 control-label">Host</label>
<div class="col-sm-9">
<div class="col-sm-7">
<input type="text" id="pg_host" class="form-control" />
</div>
<div class="col-sm-2 no-left-padding">
<input type="text" id="pg_port" class="form-control" placeholder="5432" />
</div>
</div>
<div class="form-group">
@@ -172,14 +190,7 @@
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Port</label>
<div class="col-sm-9">
<input type="text" id="pg_port" class="form-control" placeholder="5432" />
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">SSL</label>
<label class="col-sm-3 control-label">SSL Mode</label>
<div class="col-sm-9">
<select class="form-control" id="connection_ssl">
<option value="disable">disable</option>
@@ -191,11 +202,16 @@
</div>
<div class="connection-ssh-group">
<hr/>
<div class="form-group">
<label class="col-sm-3 control-label">SSH Host</label>
<div class="col-sm-9">
<div class="col-sm-7">
<input type="text" id="ssh_host" class="form-control" />
</div>
<div class="col-sm-2 no-left-padding">
<input type="text" id="ssh_port" class="form-control" placeholder="22" />
</div>
</div>
<div class="form-group">
@@ -213,9 +229,9 @@
</div>
<div class="form-group">
<label class="col-sm-3 control-label">SSH Port</label>
<label class="col-sm-3 control-label">SSH Key</label>
<div class="col-sm-9">
<input type="text" id="ssh_port" class="form-control" placeholder="optional" />
<input type="text" id="ssh_key" class="form-control" placeholder="optional" />
</div>
</div>
</div>
@@ -233,12 +249,45 @@
</div>
<div id="tables_context_menu">
<ul class="dropdown-menu" role="menu">
<li><a href="#" data-action="copy">Copy Table Name</a></li>
<li class="divider"></li>
<li><a href="#" data-action="export" data-format="json">Export to JSON</a></li>
<li><a href="#" data-action="export" data-format="csv">Export to CSV</a></li>
<li><a href="#" data-action="export" data-format="xml">Export to XML</a></li>
<li><a href="#" data-action="dump">Export to SQL</a></li>
<li class="divider"></li>
<li><a href="#" data-action="truncate">Truncate Table</a></li>
<li><a href="#" data-action="delete">Delete Table</a></li>
</ul>
</div>
<div id="view_context_menu">
<ul class="dropdown-menu" role="menu">
<li><a href="#" data-action="copy">Copy View Name</a></li>
<li class="divider"></li>
<li><a href="#" data-action="export" data-format="json">Export to JSON</a></li>
<li><a href="#" data-action="export" data-format="csv">Export to CSV</a></li>
<li><a href="#" data-action="export" data-format="xml">Export to XML</a></li>
<li class="divider"></li>
<li><a href="#" data-action="truncate">Truncate table</a></li>
<li><a href="#" data-action="delete">Delete table</a></li>
<li><a href="#" data-action="delete">Delete View</a></li>
</ul>
</div>
<div id="current_database_context_menu">
<ul class="dropdown-menu" role="menu">
<li><a href="#" data-action="export">Export SQL dump</a></li>
</ul>
</div>
<div id="results_header_menu">
<ul class="dropdown-menu" role="menu">
<li><a href="#" data-action="unique_values" data-counts="false">Unique Values</a></li>
<li><a href="#" data-action="unique_values" data-counts="true">Unique Values + Counts</a></li>
<li><a href="#" data-action="num_stats">Numeric stats (min/max/avg)</a></li>
<li><a href="#" data-action="copy_name">Copy Column Name</a></li>
</ul>
</div>
<div id="results_row_menu">
<ul class="dropdown-menu" role="menu">
<li><a href="#" data-action="copy_value">Copy Value</a></li>
<li><a href="#" data-action="filter_by_value">Filter Rows By Value</a></li>
</ul>
</div>
</body>
+458 -77
View File
@@ -17,11 +17,6 @@ var filterOptions = {
"not_null": "IS NOT NULL"
};
function guid() {
function s4() { return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); }
return [s4(), s4(), "-", s4(), "-", s4(), "-", s4(), "-", s4(), s4(), s4()].join("");
}
function getSessionId() {
var id = sessionStorage.getItem("session_id");
@@ -83,6 +78,7 @@ function apiCall(method, path, params, cb) {
});
}
function getInfo(cb) { apiCall("get", "/info", {}, cb); }
function getObjects(cb) { apiCall("get", "/objects", {}, cb); }
function getTables(cb) { apiCall("get", "/tables", {}, cb); }
function getTableRows(table, opts, cb) { apiCall("get", "/tables/" + table + "/rows", opts, cb); }
@@ -96,7 +92,7 @@ function explainQuery(query, cb) { apiCall("post", "/explain", { quer
function disconnect(cb) { apiCall("post", "/disconnect", {}, cb); }
function encodeQuery(query) {
return window.btoa(query).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ".");
return Base64.encode(query).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ".");
}
function buildSchemaSection(name, objects) {
@@ -123,22 +119,22 @@ function buildSchemaSection(name, objects) {
section += "<div class='schema-name'><i class='fa fa-folder-o'></i><i class='fa fa-folder-open-o'></i> " + name + "</div>";
section += "<div class='schema-container'>";
for (group of ["table", "view", "materialized_view", "sequence"]) {
if (objects[group].length == 0) continue;
["table", "view", "materialized_view", "sequence"].forEach(function(group) {
group_klass = "";
if (name == "public" && group == "table") group_klass = "expanded";
section += "<div class='schema-group " + group_klass + "'>";
section += "<div class='schema-group-title'><i class='fa fa-chevron-right'></i><i class='fa fa-chevron-down'></i> " + titles[group] + " (" + objects[group].length + ")</div>";
section += "<ul>"
section += "<div class='schema-group-title'><i class='fa fa-chevron-right'></i><i class='fa fa-chevron-down'></i> " + titles[group] + " <span class='schema-group-count'>" + objects[group].length + "</span></div>";
section += "<ul data-group='" + group + "'>";
for (item of objects[group]) {
var id = name + "." + item;
section += "<li class='schema-" + group + "' data-type='" + group + "' data-id='" + id + "'>" + icons[group] + "&nbsp;" + item + "</li>";
if (objects[group]) {
objects[group].forEach(function(item) {
var id = name + "." + item;
section += "<li class='schema-" + group + "' data-type='" + group + "' data-id='" + id + "'>" + icons[group] + "&nbsp;" + item + "</li>";
});
section += "</ul></div>";
}
section += "</ul></div>";
}
});
section += "</div></div>";
@@ -149,6 +145,15 @@ function loadSchemas() {
$("#objects").html("");
getObjects(function(data) {
if (Object.keys(data).length == 0) {
data["public"] = {
table: [],
view: [],
materialized_view: [],
sequence: []
};
}
for (schema in data) {
$(buildSchemaSection(schema, data[schema])).appendTo("#objects");
}
@@ -181,10 +186,12 @@ function getCurrentObject() {
function resetTable() {
$("#results").
attr("data-mode", "").
text("").
data("mode", "").
removeClass("empty").
removeClass("no-crop");
$("#results_header").html("");
$("#results_body").html("");
}
function performTableAction(table, action, el) {
@@ -209,12 +216,60 @@ function performTableAction(table, action, el) {
break;
case "export":
var format = el.data("format");
var filename = table + "." + format;
var db = $("#current_database").text();
var filename = db + "." + table + "." + format;
var query = window.encodeURI("SELECT * FROM " + table);
var url = "http://" + window.location.host + "/api/query?format=" + format + "&filename=" + filename + "&query=" + query + "&_session_id=" + getSessionId();
var url = window.location.href.split("#")[0] + "api/query?format=" + format + "&filename=" + filename + "&query=" + query + "&_session_id=" + getSessionId();
var win = window.open(url, "_blank");
win.focus();
break;
case "dump":
var url = window.location.href.split("#")[0] + "api/export?table=" + table + "&_session_id=" + getSessionId();
var win = window.open(url, "_blank");
win.focus();
break;
case "copy":
copyToClipboard(table.split('.')[1]);
break;
}
}
function performViewAction(view, action, el) {
if (action == "delete") {
var message = "Are you sure you want to " + action + " view " + view + " ?";
if (!confirm(message)) return;
}
switch(action) {
case "delete":
executeQuery("DROP VIEW " + view, function(data) {
if (data.error) alert(data.error);
loadSchemas();
resetTable();
});
break;
case "export":
var format = el.data("format");
var db = $("#current_database").text();
var filename = db + "." + view + "." + format;
var query = window.encodeURI("SELECT * FROM " + view);
var url = window.location.href.split("#")[0] + "api/query?format=" + format + "&filename=" + filename + "&query=" + query + "&_session_id=" + getSessionId();
var win = window.open(url, "_blank");
win.focus();
break;
case "copy":
copyToClipboard(view.split('.')[1]);
break;
}
}
function performRowAction(action, value) {
if (action == "stop_query") {
if (!confirm("Are you sure you want to stop the query?")) return;
executeQuery("SELECT pg_cancel_backend(" + value + ");", function(data) {
if (data.error) alert(data.error);
setTimeout(showActivityPanel, 1000);
});
}
}
@@ -229,17 +284,22 @@ function sortArrow(direction) {
}
}
function buildTable(results, sortColumn, sortOrder) {
function buildTable(results, sortColumn, sortOrder, options) {
if (!options) options = {};
var action = options.action;
resetTable();
if (results.error) {
$("<tr><td>ERROR: " + results.error + "</tr></tr>").appendTo("#results");
$("#results").addClass("empty");
$("#results_header").html("");
$("#results_body").html("<tr><td>ERROR: " + results.error + "</tr></tr>");
return;
}
if (results.rows.length == 0) {
$("<tr><td>No records found</tr></tr>").appendTo("#results");
$("#results_header").html("");
$("#results_body").html("<tr><td>No records found</td></tr>");
$("#result-rows-count").html("");
$("#results").addClass("empty");
return;
}
@@ -249,25 +309,55 @@ function buildTable(results, sortColumn, sortOrder) {
results.columns.forEach(function(col) {
if (col === sortColumn) {
cols += "<th class='active' data='" + col + "'" + "data-sort-order=" + sortOrder + ">" + col + "&nbsp;" + sortArrow(sortOrder) + "</th>";
cols += "<th class='table-header-col active' data-name='" + col + "'" + "data-order=" + sortOrder + ">" + col + "&nbsp;" + sortArrow(sortOrder) + "</th>";
}
else {
cols += "<th data='" + col + "'>" + col + "</th>";
cols += "<th class='table-header-col' data-name='" + col + "'>" + col + "</th>";
}
});
// No header to make the column non-sortable
if (action) {
cols += "<th></th>";
// Determine which column contains the data attribute
action.dataColumn = results.columns.indexOf(action.data);
}
results.rows.forEach(function(row) {
var r = "";
for (i in row) { r += "<td><div>" + escapeHtml(row[i]) + "</div></td>"; }
// Add all actual row data here
for (i in row) {
r += "<td data-col='" + i + "'><div>" + escapeHtml(row[i]) + "</div></td>";
}
// Add row action button
if (action) {
r += "<td><a class='btn btn-xs btn-" + action.style + " row-action' data-action='" + action.name + "' data-value='" + row[action.dataColumn] + "' href='#'>" + action.title + "</a></td>";
}
rows += "<tr>" + r + "</tr>";
});
$("<thead>" + cols + "</thead><tbody>" + rows + "</tobdy>").appendTo("#results");
$("#results_header").html(cols);
$("#results_body").html(rows);
// Show number of rows rendered on the page
$("#result-rows-count").html(results.rows.length + " rows");
}
function setCurrentTab(id) {
// Pagination should only be visible on rows tab
if (id != "table_content") {
$("#body").removeClass("with-pagination");
}
$("#nav ul li.selected").removeClass("selected");
$("#" + id).addClass("selected");
// Persist tab selection into the session storage
sessionStorage.setItem("tab", id);
}
function showQueryHistory() {
@@ -406,16 +496,30 @@ function showTableContent(sortColumn, sortOrder) {
}
getTableRows(name, opts, function(data) {
$("#results").attr("data-mode", "browse");
$("#input").hide();
$("#body").prop("class", "with-pagination");
buildTable(data, sortColumn, sortOrder);
setCurrentTab("table_content");
updatePaginator(data.pagination);
$("#results").data("mode", "browse").data("table", name);
});
}
function showPaginatedTableContent() {
var activeColumn = $("#results th.active");
var sortColumn = null;
var sortOrder = null;
if (activeColumn.length) {
sortColumn = activeColumn.data("name");
sortOrder = activeColumn.data("order");
}
showTableContent(sortColumn, sortOrder);
}
function showTableStructure() {
var name = getCurrentObject().name;
@@ -425,12 +529,10 @@ function showTableStructure() {
}
setCurrentTab("table_structure");
$("#input").hide();
$("#body").prop("class", "full");
console.log(getCurrentObject());
getTableStructure(name, { type: getCurrentObject().type }, function(data) {
buildTable(data);
$("#results").addClass("no-crop");
@@ -438,6 +540,10 @@ function showTableStructure() {
}
function showQueryPanel() {
if (!$("#table_query").hasClass("selected")) {
resetTable();
}
setCurrentTab("table_query");
editor.focus();
@@ -466,10 +572,18 @@ function showConnectionPanel() {
}
function showActivityPanel() {
setCurrentTab("table_activity");
var options = {
action: {
name: "stop_query",
title: "stop",
data: "pid",
style: "danger"
}
}
setCurrentTab("table_activity");
apiCall("get", "/activity", {}, function(data) {
buildTable(data);
buildTable(data, null, null, options);
$("#input").hide();
$("#body").addClass("full");
});
@@ -496,15 +610,14 @@ function runQuery() {
$("#query_progress").hide();
$("#input").show();
$("#body").removeClass("full");
$("#results").data("mode", "query");
if (query.toLowerCase().indexOf("explain") != -1) {
$("#results").addClass("no-crop");
}
var re = /(create|drop)\s/i;
// Reload objects list if anything was created/deleted
if (query.match(re)) {
if (query.match(/(create|drop)\s/i)) {
loadSchemas();
}
});
@@ -516,7 +629,7 @@ function runExplain() {
$("#run, #explain, #csv, #json, #xml").prop("disabled", true);
$("#query_progress").show();
var query = $.trim(editor.getValue());
var query = $.trim(editor.getSelectedText() || editor.getValue());
if (query.length == 0) {
$("#run, #explain, #csv, #json, #xml").prop("disabled", false);
@@ -536,19 +649,49 @@ function runExplain() {
}
function exportTo(format) {
var query = $.trim(editor.getValue());
var query = $.trim(editor.getSelectedText() || editor.getValue());
if (query.length == 0) {
return;
}
var url = "http://" + window.location.host + "/api/query?format=" + format + "&query=" + encodeQuery(query) + "&_session_id=" + getSessionId();
var url = window.location.href.split("#")[0] + "api/query?format=" + format + "&query=" + encodeQuery(query) + "&_session_id=" + getSessionId();
var win = window.open(url, '_blank');
setCurrentTab("table_query");
win.focus();
}
// Fetch all unique values for the selected column in the table
function showUniqueColumnsValues(table, column, showCounts) {
var query = 'SELECT DISTINCT "' + column + '" FROM ' + table;
// Display results ordered by counts.
// This could be slow on large sets without an index.
if (showCounts) {
query = 'SELECT DISTINCT "' + column + '", COUNT(1) AS total_count FROM ' + table + ' GROUP BY "' + column + '" ORDER BY total_count DESC';
}
executeQuery(query, function(data) {
$("#input").hide();
$("#body").prop("class", "full");
$("#results").data("mode", "query");
buildTable(data);
});
}
// Show numeric stats on the field
function showFieldNumStats(table, column) {
var query = 'SELECT count(1), min(' + column + '), max(' + column + '), avg(' + column + ') FROM ' + table;
executeQuery(query, function(data) {
$("#input").hide();
$("#body").prop("class", "full");
$("#results").data("mode", "query");
buildTable(data);
});
}
function buildTableFilters(name, type) {
getTableStructure(name, { type: type }, function(data) {
if (data.rows.length == 0) {
@@ -560,7 +703,9 @@ function buildTableFilters(name, type) {
$("#pagination select.column").html("<option value='' selected>Select column</option>");
for (row of data.rows) {
for (var i = 0; i < data.rows.length; i++) {
var row = data.rows[i];
var el = $("<option/>").attr("value", row[0]).text(row[0]);
$("#pagination select.column").append(el);
}
@@ -626,7 +771,36 @@ function addShortcutTooltips() {
}
}
// Get the latest release from Github API
function getLatestReleaseInfo(current) {
try {
$.get("https://api.github.com/repos/sosedoff/pgweb/releases/latest", function(release) {
if (release.name != current.version) {
var message = "Update available. Check out " + release.tag_name + " on <a target='_blank' href='" + release.html_url + "'>Github</a>";
$(".connection-settings .update").html(message).fadeIn();
}
});
}
catch(error) {
console.log("Cant get last release from github:", error);
}
}
function showConnectionSettings() {
// Fetch server info
getInfo(function(data) {
if (data.error) return;
if (!data.version) return;
// Show the current postgres version
$(".connection-settings .version").text("v" + data.version).show();
// Check for updates if running the actual release from Github
if (data.git_sha == "") {
getLatestReleaseInfo(data);
}
});
getBookmarks(function(data) {
// Do not add any bookmarks if we've got an error
if (data.error) {
@@ -687,18 +861,162 @@ function getConnectionString() {
return url;
}
function bindContextMenus() {
$(".schema-group ul").each(function(id, el) {
$(el).contextmenu({
target: "#tables_context_menu",
scopes: "li.schema-table",
onItem: function(context, e) {
var el = $(e.target);
var table = $(context[0]).data("id");
var action = el.data("action");
performTableAction(table, action, el);
// Add a context menu to the results table header columns
function bindTableHeaderMenu() {
$("#results_header").contextmenu({
scopes: "th",
target: "#results_header_menu",
before: function(e, element, target) {
// Enable menu for browsing table rows view only.
if ($("#results").data("mode") != "browse") {
e.preventDefault();
this.closemenu();
return false;
}
});
},
onItem: function(context, e) {
var menuItem = $(e.target);
switch(menuItem.data("action")) {
case "copy_name":
copyToClipboard($(context).data("name"));
break;
case "unique_values":
showUniqueColumnsValues(
$("#results").data("table"), // table name
$(context).data("name"), // column name
menuItem.data("counts") // display counts
);
break;
case "num_stats":
showFieldNumStats(
$("#results").data("table"), // table name
$(context).data("name") // column name
);
break;
}
}
});
$("#results_body").contextmenu({
scopes: "td",
target: "#results_row_menu",
before: function(e, element, target) {
var isEmpty = $("#results").hasClass("empty");
var isBrowsing = $("#results").data("mode") == "browse";
if (isEmpty || !isBrowsing) {
e.preventDefault();
this.closemenu();
return false;
}
},
onItem: function(context, e) {
var menuItem = $(e.target);
switch(menuItem.data("action")) {
case "copy_value":
copyToClipboard($(context).text());
break;
case "filter_by_value":
var colIdx = $(context).data("col");
var colValue = $(context).text();
var colName = $("#results_header th").eq(colIdx).data("name");
$("select.column").val(colName);
$("select.filter").val("equal");
$("#table_filter_value").val(colValue);
$("#rows_filter").submit();
}
}
});
}
function bindCurrentDatabaseMenu() {
$("#current_database").contextmenu({
target: "#current_database_context_menu",
onItem: function(context, e) {
var menuItem = $(e.target);
switch(menuItem.data("action")) {
case "export":
var url = window.location.href.split("#")[0] + "api/export?_session_id=" + getSessionId();
var win = window.open(url, "_blank");
win.focus();
break;
}
}
});
}
function getQuotedSchemaTableName(table) {
if (typeof table === "string" && table.indexOf(".") > -1) {
var schemaTableComponents = table.split(".");
return ['"', schemaTableComponents[0], '"."', schemaTableComponents[1], '"'].join('');
}
return table;
}
function bindContextMenus() {
bindTableHeaderMenu();
bindCurrentDatabaseMenu();
$(".schema-group ul").each(function(id, el) {
var group = $(el).data("group");
if (group == "table") {
$(el).contextmenu({
target: "#tables_context_menu",
scopes: "li.schema-table",
onItem: function(context, e) {
var el = $(e.target);
var table = getQuotedSchemaTableName($(context[0]).data("id"));
var action = el.data("action");
performTableAction(table, action, el);
}
});
}
if (group == "view") {
$(el).contextmenu({
target: "#view_context_menu",
scopes: "li.schema-view",
onItem: function(context, e) {
var el = $(e.target);
var table = getQuotedSchemaTableName($(context[0]).data("id"));
var action = el.data("action");
performViewAction(table, action, el);
}
});
}
});
}
function toggleDatabaseSearch() {
$("#current_database").toggle();
$("#database_search").toggle();
}
function enableDatabaseSearch(data) {
var input = $("#database_search");
input.typeahead("destroy");
input.typeahead({
source: data,
minLength: 0,
items: "all",
autoSelect: false,
fitToElement: true
});
input.typeahead("lookup").focus();
input.on("focusout", function(e){
toggleDatabaseSearch();
input.off("focusout");
});
}
@@ -732,7 +1050,7 @@ $(document).ready(function() {
exportTo("xml");
});
$("#results").on("click", "tr", function() {
$("#results").on("click", "tr", function(e) {
$("#results tr.selected").removeClass();
$(this).addClass("selected");
});
@@ -757,25 +1075,42 @@ $(document).ready(function() {
$(".filters select, .filters input").val("");
showTableInfo();
showTableContent();
switch(sessionStorage.getItem("tab")) {
case "table_content":
showTableContent();
break;
case "table_structure":
showTableStructure();
break;
case "table_constraints":
showTableConstraints();
break;
case "table_indexes":
showTableIndexes();
break;
default:
showTableContent();
}
});
$("#results").on("click", "a.row-action", function(e) {
e.preventDefault();
var action = $(this).data("action");
var value = $(this).data("value");
performRowAction(action, value);
})
$("#results").on("click", "th", function(e) {
var sortColumn = this.attributes['data'].value;
var contentTab = $('#table_content').hasClass('selected');
if (!$("#table_content").hasClass("selected")) return;
if (!contentTab) {
return;
}
var sortColumn = $(this).data("name");
var sortOrder = $(this).data("order") === "ASC" ? "DESC" : "ASC";
if (this.dataset.sortOrder === "ASC") {
this.dataset.sortOrder = "DESC"
}
else {
this.dataset.sortOrder = "ASC"
}
showTableContent(sortColumn, this.dataset.sortOrder);
$(this).data("order", sortOrder);
showTableContent(sortColumn, sortOrder);
});
$("#results").on("dblclick", "td > div", function() {
@@ -844,13 +1179,21 @@ $(document).ready(function() {
showTableContent();
});
// Automatically prefill the filter if it's not set yet
$("select.column").on("change", function() {
if ($("select.filter").val() == "") {
$("select.filter").val("equal");
$("#table_filter_value").focus();
}
});
$("#pagination .next-page").on("click", function() {
var current = $(".current-page").data("page");
var total = $(".current-page").data("pages");
if (total > current) {
$(".current-page").data("page", current + 1);
showTableContent();
showPaginatedTableContent();
if (current + 1 == total) {
$(this).prop("disabled", "disabled");
@@ -868,7 +1211,7 @@ $(document).ready(function() {
if (current > 1) {
$(".current-page").data("page", current - 1);
$(".next-page").prop("disabled", "");
showTableContent();
showPaginatedTableContent();
}
if (current == 1) {
@@ -876,6 +1219,26 @@ $(document).ready(function() {
}
});
$("#current_database").on("click", function(e) {
apiCall("get", "/databases", {}, function(resp) {
toggleDatabaseSearch();
enableDatabaseSearch(resp);
});
});
$("#database_search").change(function(e) {
var current = $("#database_search").typeahead("getActive");
if (current && current == $("#database_search").val()) {
apiCall("post", "/switchdb", { db: current }, function(resp) {
if (resp.error) {
alert(resp.error);
return;
};
window.location.reload();
});
};
});
$("#edit_connection").on("click", function() {
if (connected) {
$("#close_connection_window").show();
@@ -885,8 +1248,11 @@ $(document).ready(function() {
});
$("#close_connection").on("click", function() {
if (!confirm("Are you sure you want to disconnect?")) return;
disconnect(function() {
showConnectionSettings();
resetTable();
$("#close_connection_window").hide();
});
});
@@ -934,15 +1300,12 @@ $(document).ready(function() {
$("#connection_bookmarks").on("change", function(e) {
var name = $.trim($(this).val());
if (name == "") return;
if (name == "") {
return;
}
item = bookmarks[name];
var item = bookmarks[name];
// Check if bookmark only has url set
if (item.url != "") {
if (item.url && item.url != "") {
$("#connection_url").val(item.url);
$("#connection_scheme").click();
return;
@@ -955,12 +1318,13 @@ $(document).ready(function() {
$("#pg_password").val(item.password);
$("#pg_db").val(item.database);
$("#connection_ssl").val(item.ssl);
if (Object.keys(item.ssh).length > 0) {
if (item.ssh && Object.keys(item.ssh).length > 0) {
$("#ssh_host").val(item.ssh.host);
$("#ssh_port").val(item.ssh.port);
$("#ssh_user").val(item.ssh.user);
$("#ssh_password").val(item.ssh.password);
$("#ssh_key").val(item.ssh.key);
$("#connection_ssh").click();
}
else {
@@ -968,7 +1332,9 @@ $(document).ready(function() {
$("#ssh_port").val("");
$("#ssh_user").val("");
$("#ssh_password").val("");
$("#ssh_key").val("");
$(".connection-ssh-group").hide();
$("#connection_standard").click();
}
});
@@ -990,6 +1356,7 @@ $(document).ready(function() {
params["ssh_port"] = $("#ssh_port").val();
params["ssh_user"] = $("#ssh_user").val();
params["ssh_password"] = $("#ssh_password").val();
params["ssh_key"] = $("#ssh_key").val();
}
$("#connection_error").hide();
@@ -1016,10 +1383,20 @@ $(document).ready(function() {
initEditor();
addShortcutTooltips();
// Set session from the url
var reqUrl = new URL(window.location);
var sessionId = reqUrl.searchParams.get("session");
if (sessionId && sessionId != "") {
sessionStorage.setItem("session_id", sessionId);
window.history.pushState({}, document.title, window.location.pathname);
}
apiCall("get", "/connection", {}, function(resp) {
if (resp.error) {
connected = false;
showConnectionSettings();
$(".connection-actions").show();
}
else {
connected = true;
@@ -1027,6 +1404,10 @@ $(document).ready(function() {
$("#current_database").text(resp.current_database);
$("#main").show();
if (!resp.session_lock) {
$(".connection-actions").show();
}
}
});
});
+107
View File
@@ -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;
}
}
File diff suppressed because one or more lines are too long
+28
View File
@@ -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
View File
@@ -2,6 +2,10 @@ language: go
go:
- 1.1
- 1.2
- 1.3
- 1.4
- 1.5
- 1.6
- tip
install:
- go install ./...
@@ -9,4 +13,3 @@ install:
script:
- export PATH="$PATH:$HOME/gopath/bin"
- make test
+1 -1
View File
@@ -1,3 +1,3 @@
Compatible with TOML version
[v0.2.0](https://github.com/mojombo/toml/blob/master/versions/toml-v0.2.0.md)
[v0.4.0](https://github.com/toml-lang/toml/blob/v0.4.0/versions/en/toml-v0.4.0.md)
+8 -10
View File
@@ -1,17 +1,17 @@
## TOML parser and encoder for Go with reflection
TOML stands for Tom's Obvious, Minimal Language. This Go package provides a
reflection interface similar to Go's standard library `json` and `xml`
reflection interface similar to Go's standard library `json` and `xml`
packages. This package also supports the `encoding.TextUnmarshaler` and
`encoding.TextMarshaler` interfaces so that you can define custom data
`encoding.TextMarshaler` interfaces so that you can define custom data
representations. (There is an example of this below.)
Spec: https://github.com/mojombo/toml
Spec: https://github.com/toml-lang/toml
Compatible with TOML version
[v0.2.0](https://github.com/mojombo/toml/blob/master/versions/toml-v0.2.0.md)
[v0.4.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md)
Documentation: http://godoc.org/github.com/BurntSushi/toml
Documentation: https://godoc.org/github.com/BurntSushi/toml
Installation:
@@ -26,8 +26,7 @@ go get github.com/BurntSushi/toml/cmd/tomlv
tomlv some-toml-file.toml
```
[![Build status](https://api.travis-ci.org/BurntSushi/toml.png)](https://travis-ci.org/BurntSushi/toml)
[![Build Status](https://travis-ci.org/BurntSushi/toml.svg?branch=master)](https://travis-ci.org/BurntSushi/toml) [![GoDoc](https://godoc.org/github.com/BurntSushi/toml?status.svg)](https://godoc.org/github.com/BurntSushi/toml)
### Testing
@@ -87,7 +86,7 @@ type TOML struct {
### Using the `encoding.TextUnmarshaler` interface
Here's an example that automatically parses duration strings into
Here's an example that automatically parses duration strings into
`time.Duration` values:
```toml
@@ -120,7 +119,7 @@ for _, s := range favorites.Song {
}
```
And you'll also need a `duration` type that satisfies the
And you'll also need a `duration` type that satisfies the
`encoding.TextUnmarshaler` interface:
```go
@@ -217,4 +216,3 @@ Note that a case insensitive match will be tried if an exact match can't be
found.
A working example of the above can be found in `_examples/example.{go,toml}`.
+14
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -10,7 +10,9 @@ import (
"time"
)
var e = fmt.Errorf
func e(format string, args ...interface{}) error {
return fmt.Errorf("toml: "+format, args...)
}
// Unmarshaler is the interface implemented by objects that can unmarshal a
// TOML description of themselves.
@@ -103,6 +105,13 @@ func (md *MetaData) PrimitiveDecode(primValue Primitive, v interface{}) error {
// This decoder will not handle cyclic types. If a cyclic type is passed,
// `Decode` will not terminate.
func Decode(data string, v interface{}) (MetaData, error) {
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Ptr {
return MetaData{}, e("Decode of non-pointer %s", reflect.TypeOf(v))
}
if rv.IsNil() {
return MetaData{}, e("Decode of nil %s", reflect.TypeOf(v))
}
p, err := parse(data)
if err != nil {
return MetaData{}, err
@@ -111,7 +120,7 @@ func Decode(data string, v interface{}) (MetaData, error) {
p.mapping, p.types, p.ordered,
make(map[string]bool, len(p.ordered)), nil,
}
return md, md.unify(p.mapping, rvalue(v))
return md, md.unify(p.mapping, indirect(rv))
}
// DecodeFile is just like Decode, except it will automatically read the
@@ -211,7 +220,7 @@ func (md *MetaData) unify(data interface{}, rv reflect.Value) error {
case reflect.Interface:
// we only support empty interfaces.
if rv.NumMethod() > 0 {
return e("Unsupported type '%s'.", rv.Kind())
return e("unsupported type %s", rv.Type())
}
return md.unifyAnything(data, rv)
case reflect.Float32:
@@ -219,13 +228,17 @@ func (md *MetaData) unify(data interface{}, rv reflect.Value) error {
case reflect.Float64:
return md.unifyFloat64(data, rv)
}
return e("Unsupported type '%s'.", rv.Kind())
return e("unsupported type %s", rv.Kind())
}
func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error {
tmap, ok := mapping.(map[string]interface{})
if !ok {
return mismatch(rv, "map", mapping)
if mapping == nil {
return nil
}
return e("type mismatch for %s: expected table but found %T",
rv.Type().String(), mapping)
}
for key, datum := range tmap {
@@ -250,14 +263,13 @@ func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error {
md.decoded[md.context.add(key).String()] = true
md.context = append(md.context, key)
if err := md.unify(datum, subv); err != nil {
return e("Type mismatch for '%s.%s': %s",
rv.Type().String(), f.name, err)
return err
}
md.context = md.context[0 : len(md.context)-1]
} else if f.name != "" {
// Bad user! No soup for you!
return e("Field '%s.%s' is unexported, and therefore cannot "+
"be loaded with reflection.", rv.Type().String(), f.name)
return e("cannot write unexported field %s.%s",
rv.Type().String(), f.name)
}
}
}
@@ -267,6 +279,9 @@ func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error {
func (md *MetaData) unifyMap(mapping interface{}, rv reflect.Value) error {
tmap, ok := mapping.(map[string]interface{})
if !ok {
if tmap == nil {
return nil
}
return badtype("map", mapping)
}
if rv.IsNil() {
@@ -292,6 +307,9 @@ func (md *MetaData) unifyMap(mapping interface{}, rv reflect.Value) error {
func (md *MetaData) unifyArray(data interface{}, rv reflect.Value) error {
datav := reflect.ValueOf(data)
if datav.Kind() != reflect.Slice {
if !datav.IsValid() {
return nil
}
return badtype("slice", data)
}
sliceLen := datav.Len()
@@ -305,12 +323,16 @@ func (md *MetaData) unifyArray(data interface{}, rv reflect.Value) error {
func (md *MetaData) unifySlice(data interface{}, rv reflect.Value) error {
datav := reflect.ValueOf(data)
if datav.Kind() != reflect.Slice {
if !datav.IsValid() {
return nil
}
return badtype("slice", data)
}
sliceLen := datav.Len()
if rv.IsNil() {
rv.Set(reflect.MakeSlice(rv.Type(), sliceLen, sliceLen))
n := datav.Len()
if rv.IsNil() || rv.Cap() < n {
rv.Set(reflect.MakeSlice(rv.Type(), n, n))
}
rv.SetLen(n)
return md.unifySliceArray(datav, rv)
}
@@ -365,15 +387,15 @@ func (md *MetaData) unifyInt(data interface{}, rv reflect.Value) error {
// No bounds checking necessary.
case reflect.Int8:
if num < math.MinInt8 || num > math.MaxInt8 {
return e("Value '%d' is out of range for int8.", num)
return e("value %d is out of range for int8", num)
}
case reflect.Int16:
if num < math.MinInt16 || num > math.MaxInt16 {
return e("Value '%d' is out of range for int16.", num)
return e("value %d is out of range for int16", num)
}
case reflect.Int32:
if num < math.MinInt32 || num > math.MaxInt32 {
return e("Value '%d' is out of range for int32.", num)
return e("value %d is out of range for int32", num)
}
}
rv.SetInt(num)
@@ -384,15 +406,15 @@ func (md *MetaData) unifyInt(data interface{}, rv reflect.Value) error {
// No bounds checking necessary.
case reflect.Uint8:
if num < 0 || unum > math.MaxUint8 {
return e("Value '%d' is out of range for uint8.", num)
return e("value %d is out of range for uint8", num)
}
case reflect.Uint16:
if num < 0 || unum > math.MaxUint16 {
return e("Value '%d' is out of range for uint16.", num)
return e("value %d is out of range for uint16", num)
}
case reflect.Uint32:
if num < 0 || unum > math.MaxUint32 {
return e("Value '%d' is out of range for uint32.", num)
return e("value %d is out of range for uint32", num)
}
}
rv.SetUint(unum)
@@ -458,7 +480,7 @@ func rvalue(v interface{}) reflect.Value {
// interest to us (like encoding.TextUnmarshaler).
func indirect(v reflect.Value) reflect.Value {
if v.Kind() != reflect.Ptr {
if v.CanAddr() {
if v.CanSet() {
pv := v.Addr()
if _, ok := pv.Interface().(TextUnmarshaler); ok {
return pv
@@ -483,10 +505,5 @@ func isUnifiable(rv reflect.Value) bool {
}
func badtype(expected string, data interface{}) error {
return e("Expected %s but found '%T'.", expected, data)
}
func mismatch(user reflect.Value, expected string, data interface{}) error {
return e("Type mismatch for %s. Expected %s but found '%T'.",
user.Type().String(), expected, data)
return e("cannot load TOML value of type %T into a Go %s", data, expected)
}
+22
View File
@@ -59,6 +59,28 @@ func (k Key) String() string {
return strings.Join(k, ".")
}
func (k Key) maybeQuotedAll() string {
var ss []string
for i := range k {
ss = append(ss, k.maybeQuoted(i))
}
return strings.Join(ss, ".")
}
func (k Key) maybeQuoted(i int) string {
quote := false
for _, c := range k[i] {
if !isBareKeyChar(c) {
quote = true
break
}
}
if quote {
return "\"" + strings.Replace(k[i], "\"", "\\\"", -1) + "\""
}
return k[i]
}
func (k Key) add(piece string) Key {
newKey := make(Key, len(k)+1)
copy(newKey, k)
+1 -1
View File
@@ -4,7 +4,7 @@ files via reflection. There is also support for delaying decoding with
the Primitive type, and querying the set of keys in a TOML document with the
MetaData type.
The specification implemented: https://github.com/mojombo/toml
The specification implemented: https://github.com/toml-lang/toml
The sub-command github.com/BurntSushi/toml/cmd/tomlv can be used to verify
whether a file is a valid TOML document. It can also be used to print the
+118 -65
View File
@@ -16,17 +16,17 @@ type tomlEncodeError struct{ error }
var (
errArrayMixedElementTypes = errors.New(
"can't encode array with mixed element types")
"toml: cannot encode array with mixed element types")
errArrayNilElement = errors.New(
"can't encode array with nil element")
"toml: cannot encode array with nil element")
errNonString = errors.New(
"can't encode a map with non-string key type")
"toml: cannot encode a map with non-string key type")
errAnonNonStruct = errors.New(
"can't encode an anonymous field that is not a struct")
"toml: cannot encode an anonymous field that is not a struct")
errArrayNoTable = errors.New(
"TOML array element can't contain a table")
"toml: TOML array element cannot contain a table")
errNoKey = errors.New(
"top-level values must be a Go map or struct")
"toml: top-level values must be Go maps or structs")
errAnything = errors.New("") // used in testing
)
@@ -118,7 +118,8 @@ func (enc *Encoder) encode(key Key, rv reflect.Value) {
k := rv.Kind()
switch k {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
reflect.Uint64,
reflect.Float32, reflect.Float64, reflect.String, reflect.Bool:
@@ -147,7 +148,7 @@ func (enc *Encoder) encode(key Key, rv reflect.Value) {
case reflect.Struct:
enc.eTable(key, rv)
default:
panic(e("Unsupported type for key '%s': %s", key, k))
panic(e("unsupported type for key '%s': %s", key, k))
}
}
@@ -159,7 +160,7 @@ func (enc *Encoder) eElement(rv reflect.Value) {
// Special case time.Time as a primitive. Has to come before
// TextMarshaler below because time.Time implements
// encoding.TextMarshaler, but we need to always use UTC.
enc.wf(v.In(time.FixedZone("UTC", 0)).Format("2006-01-02T15:04:05Z"))
enc.wf(v.UTC().Format("2006-01-02T15:04:05Z"))
return
case TextMarshaler:
// Special case. Use text marshaler if it's available for this value.
@@ -173,7 +174,8 @@ func (enc *Encoder) eElement(rv reflect.Value) {
switch rv.Kind() {
case reflect.Bool:
enc.wf(strconv.FormatBool(rv.Bool()))
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
reflect.Int64:
enc.wf(strconv.FormatInt(rv.Int(), 10))
case reflect.Uint, reflect.Uint8, reflect.Uint16,
reflect.Uint32, reflect.Uint64:
@@ -189,7 +191,7 @@ func (enc *Encoder) eElement(rv reflect.Value) {
case reflect.String:
enc.writeQuoted(rv.String())
default:
panic(e("Unexpected primitive type: %s", rv.Kind()))
panic(e("unexpected primitive type: %s", rv.Kind()))
}
}
@@ -223,28 +225,28 @@ func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) {
if len(key) == 0 {
encPanic(errNoKey)
}
panicIfInvalidKey(key, true)
for i := 0; i < rv.Len(); i++ {
trv := rv.Index(i)
if isNil(trv) {
continue
}
panicIfInvalidKey(key)
enc.newline()
enc.wf("%s[[%s]]", enc.indentStr(key), key.String())
enc.wf("%s[[%s]]", enc.indentStr(key), key.maybeQuotedAll())
enc.newline()
enc.eMapOrStruct(key, trv)
}
}
func (enc *Encoder) eTable(key Key, rv reflect.Value) {
panicIfInvalidKey(key)
if len(key) == 1 {
// Output an extra new line between top-level tables.
// Output an extra newline between top-level tables.
// (The newline isn't written if nothing else has been written though.)
enc.newline()
}
if len(key) > 0 {
panicIfInvalidKey(key, true)
enc.wf("%s[%s]", enc.indentStr(key), key.String())
enc.wf("%s[%s]", enc.indentStr(key), key.maybeQuotedAll())
enc.newline()
}
enc.eMapOrStruct(key, rv)
@@ -304,19 +306,36 @@ func (enc *Encoder) eStruct(key Key, rv reflect.Value) {
addFields = func(rt reflect.Type, rv reflect.Value, start []int) {
for i := 0; i < rt.NumField(); i++ {
f := rt.Field(i)
// skip unexporded fields
if f.PkgPath != "" {
// skip unexported fields
if f.PkgPath != "" && !f.Anonymous {
continue
}
frv := rv.Field(i)
if f.Anonymous {
frv := eindirect(frv)
t := frv.Type()
if t.Kind() != reflect.Struct {
encPanic(errAnonNonStruct)
t := f.Type
switch t.Kind() {
case reflect.Struct:
// Treat anonymous struct fields with
// tag names as though they are not
// anonymous, like encoding/json does.
if getOptions(f.Tag).name == "" {
addFields(t, frv, f.Index)
continue
}
case reflect.Ptr:
if t.Elem().Kind() == reflect.Struct &&
getOptions(f.Tag).name == "" {
if !frv.IsNil() {
addFields(t.Elem(), frv.Elem(), f.Index)
}
continue
}
// Fall through to the normal field encoding logic below
// for non-struct anonymous fields.
}
addFields(t, frv, f.Index)
} else if typeIsHash(tomlTypeOfGo(frv)) {
}
if typeIsHash(tomlTypeOfGo(frv)) {
fieldsSub = append(fieldsSub, append(start, f.Index...))
} else {
fieldsDirect = append(fieldsDirect, append(start, f.Index...))
@@ -334,13 +353,21 @@ func (enc *Encoder) eStruct(key Key, rv reflect.Value) {
continue
}
keyName := sft.Tag.Get("toml")
if keyName == "-" {
opts := getOptions(sft.Tag)
if opts.skip {
continue
}
if keyName == "" {
keyName = sft.Name
keyName := sft.Name
if opts.name != "" {
keyName = opts.name
}
if opts.omitempty && isEmpty(sf) {
continue
}
if opts.omitzero && isZero(sf) {
continue
}
enc.encode(key.add(keyName), sf)
}
}
@@ -348,10 +375,10 @@ func (enc *Encoder) eStruct(key Key, rv reflect.Value) {
writeFields(fieldsSub)
}
// tomlTypeName returns the TOML type name of the Go value's type. It is used to
// determine whether the types of array elements are mixed (which is forbidden).
// If the Go value is nil, then it is illegal for it to be an array element, and
// valueIsNil is returned as true.
// tomlTypeName returns the TOML type name of the Go value's type. It is
// used to determine whether the types of array elements are mixed (which is
// forbidden). If the Go value is nil, then it is illegal for it to be an array
// element, and valueIsNil is returned as true.
// Returns the TOML type of a Go value. The type may be `nil`, which means
// no concrete TOML type could be found.
@@ -362,7 +389,8 @@ func tomlTypeOfGo(rv reflect.Value) tomlType {
switch rv.Kind() {
case reflect.Bool:
return tomlBool
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
reflect.Uint64:
return tomlInteger
@@ -371,9 +399,8 @@ func tomlTypeOfGo(rv reflect.Value) tomlType {
case reflect.Array, reflect.Slice:
if typeEqual(tomlHash, tomlArrayType(rv)) {
return tomlArrayHash
} else {
return tomlArray
}
return tomlArray
case reflect.Ptr, reflect.Interface:
return tomlTypeOfGo(rv.Elem())
case reflect.String:
@@ -430,6 +457,54 @@ func tomlArrayType(rv reflect.Value) tomlType {
return firstType
}
type tagOptions struct {
skip bool // "-"
name string
omitempty bool
omitzero bool
}
func getOptions(tag reflect.StructTag) tagOptions {
t := tag.Get("toml")
if t == "-" {
return tagOptions{skip: true}
}
var opts tagOptions
parts := strings.Split(t, ",")
opts.name = parts[0]
for _, s := range parts[1:] {
switch s {
case "omitempty":
opts.omitempty = true
case "omitzero":
opts.omitzero = true
}
}
return opts
}
func isZero(rv reflect.Value) bool {
switch rv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return rv.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return rv.Uint() == 0
case reflect.Float32, reflect.Float64:
return rv.Float() == 0.0
}
return false
}
func isEmpty(rv reflect.Value) bool {
switch rv.Kind() {
case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
return rv.Len() == 0
case reflect.Bool:
return !rv.Bool()
}
return false
}
func (enc *Encoder) newline() {
if enc.hasWritten {
enc.wf("\n")
@@ -440,8 +515,8 @@ func (enc *Encoder) keyEqElement(key Key, val reflect.Value) {
if len(key) == 0 {
encPanic(errNoKey)
}
panicIfInvalidKey(key, false)
enc.wf("%s%s = ", enc.indentStr(key), key[len(key)-1])
panicIfInvalidKey(key)
enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1))
enc.eElement(val)
enc.newline()
}
@@ -479,37 +554,15 @@ func isNil(rv reflect.Value) bool {
}
}
func panicIfInvalidKey(key Key, hash bool) {
if hash {
for _, k := range key {
if !isValidTableName(k) {
encPanic(e("Key '%s' is not a valid table name. Table names "+
"cannot contain '[', ']' or '.'.", key.String()))
}
}
} else {
if !isValidKeyName(key[len(key)-1]) {
encPanic(e("Key '%s' is not a name. Key names "+
"cannot contain whitespace.", key.String()))
func panicIfInvalidKey(key Key) {
for _, k := range key {
if len(k) == 0 {
encPanic(e("Key '%s' is not a valid table name. Key names "+
"cannot be empty.", key.maybeQuotedAll()))
}
}
}
func isValidTableName(s string) bool {
if len(s) == 0 {
return false
}
for _, r := range s {
if r == '[' || r == ']' || r == '.' {
return false
}
}
return true
}
func isValidKeyName(s string) bool {
if len(s) == 0 {
return false
}
return true
return len(s) != 0
}
+2 -2
View File
@@ -14,6 +14,6 @@ import (
// so that Go 1.1 can be supported.
type TextMarshaler encoding.TextMarshaler
// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined here
// so that Go 1.1 can be supported.
// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined
// here so that Go 1.1 can be supported.
type TextUnmarshaler encoding.TextUnmarshaler
+2 -2
View File
@@ -11,8 +11,8 @@ type TextMarshaler interface {
MarshalText() (text []byte, err error)
}
// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined here
// so that Go 1.1 can be supported.
// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined
// here so that Go 1.1 can be supported.
type TextUnmarshaler interface {
UnmarshalText(text []byte) error
}
+418 -328
View File
File diff suppressed because it is too large Load Diff
+215 -67
View File
@@ -2,7 +2,6 @@ package toml
import (
"fmt"
"log"
"strconv"
"strings"
"time"
@@ -67,7 +66,7 @@ func parse(data string) (p *parser, err error) {
}
func (p *parser) panicf(format string, v ...interface{}) {
msg := fmt.Sprintf("Near line %d, key '%s': %s",
msg := fmt.Sprintf("Near line %d (last key parsed '%s'): %s",
p.approxLine, p.current(), fmt.Sprintf(format, v...))
panic(parseError(msg))
}
@@ -75,13 +74,13 @@ func (p *parser) panicf(format string, v ...interface{}) {
func (p *parser) next() item {
it := p.lx.nextItem()
if it.typ == itemError {
p.panicf("Near line %d: %s", it.line, it.val)
p.panicf("%s", it.val)
}
return it
}
func (p *parser) bug(format string, v ...interface{}) {
log.Fatalf("BUG: %s\n\n", fmt.Sprintf(format, v...))
panic(fmt.Sprintf("BUG: "+format+"\n\n", v...))
}
func (p *parser) expect(typ itemType) item {
@@ -102,12 +101,12 @@ func (p *parser) topLevel(item item) {
p.approxLine = item.line
p.expect(itemText)
case itemTableStart:
kg := p.expect(itemText)
kg := p.next()
p.approxLine = kg.line
key := make(Key, 0)
for ; kg.typ == itemText; kg = p.next() {
key = append(key, kg.val)
var key Key
for ; kg.typ != itemTableEnd && kg.typ != itemEOF; kg = p.next() {
key = append(key, p.keyString(kg))
}
p.assertEqual(itemTableEnd, kg.typ)
@@ -115,12 +114,12 @@ func (p *parser) topLevel(item item) {
p.setType("", tomlHash)
p.ordered = append(p.ordered, key)
case itemArrayTableStart:
kg := p.expect(itemText)
kg := p.next()
p.approxLine = kg.line
key := make(Key, 0)
for ; kg.typ == itemText; kg = p.next() {
key = append(key, kg.val)
var key Key
for ; kg.typ != itemArrayTableEnd && kg.typ != itemEOF; kg = p.next() {
key = append(key, p.keyString(kg))
}
p.assertEqual(itemArrayTableEnd, kg.typ)
@@ -128,29 +127,44 @@ func (p *parser) topLevel(item item) {
p.setType("", tomlArrayHash)
p.ordered = append(p.ordered, key)
case itemKeyStart:
kname := p.expect(itemText)
p.currentKey = kname.val
kname := p.next()
p.approxLine = kname.line
p.currentKey = p.keyString(kname)
val, typ := p.value(p.next())
p.setValue(p.currentKey, val)
p.setType(p.currentKey, typ)
p.ordered = append(p.ordered, p.context.add(p.currentKey))
p.currentKey = ""
default:
p.bug("Unexpected type at top level: %s", item.typ)
}
}
// Gets a string for a key (or part of a key in a table name).
func (p *parser) keyString(it item) string {
switch it.typ {
case itemText:
return it.val
case itemString, itemMultilineString,
itemRawString, itemRawMultilineString:
s, _ := p.value(it)
return s.(string)
default:
p.bug("Unexpected key type: %s", it.typ)
panic("unreachable")
}
}
// value translates an expected value from the lexer into a Go value wrapped
// as an empty interface.
func (p *parser) value(it item) (interface{}, tomlType) {
switch it.typ {
case itemString:
return p.replaceUnicode(replaceEscapes(it.val)), p.typeOfPrimitive(it)
return p.replaceEscapes(it.val), p.typeOfPrimitive(it)
case itemMultilineString:
return p.replaceUnicode(replaceEscapes(stripFirstNewline(stripEscapedWhitespace(it.val)))), p.typeOfPrimitive(it)
trimmed := stripFirstNewline(stripEscapedWhitespace(it.val))
return p.replaceEscapes(trimmed), p.typeOfPrimitive(it)
case itemRawString:
return it.val, p.typeOfPrimitive(it)
case itemRawMultilineString:
@@ -164,10 +178,18 @@ func (p *parser) value(it item) (interface{}, tomlType) {
}
p.bug("Expected boolean value, but got '%s'.", it.val)
case itemInteger:
num, err := strconv.ParseInt(it.val, 10, 64)
if !numUnderscoresOK(it.val) {
p.panicf("Invalid integer %q: underscores must be surrounded by digits",
it.val)
}
val := strings.Replace(it.val, "_", "", -1)
num, err := strconv.ParseInt(val, 10, 64)
if err != nil {
// See comment below for floats describing why we make a
// distinction between a bug and a user error.
// Distinguish integer values. Normally, it'd be a bug if the lexer
// provides an invalid integer, but it's possible that the number is
// out of range of valid values (which the lexer cannot determine).
// So mark the former as a bug but the latter as a legitimate user
// error.
if e, ok := err.(*strconv.NumError); ok &&
e.Err == strconv.ErrRange {
@@ -179,29 +201,57 @@ func (p *parser) value(it item) (interface{}, tomlType) {
}
return num, p.typeOfPrimitive(it)
case itemFloat:
num, err := strconv.ParseFloat(it.val, 64)
parts := strings.FieldsFunc(it.val, func(r rune) bool {
switch r {
case '.', 'e', 'E':
return true
}
return false
})
for _, part := range parts {
if !numUnderscoresOK(part) {
p.panicf("Invalid float %q: underscores must be "+
"surrounded by digits", it.val)
}
}
if !numPeriodsOK(it.val) {
// As a special case, numbers like '123.' or '1.e2',
// which are valid as far as Go/strconv are concerned,
// must be rejected because TOML says that a fractional
// part consists of '.' followed by 1+ digits.
p.panicf("Invalid float %q: '.' must be followed "+
"by one or more digits", it.val)
}
val := strings.Replace(it.val, "_", "", -1)
num, err := strconv.ParseFloat(val, 64)
if err != nil {
// Distinguish float values. Normally, it'd be a bug if the lexer
// provides an invalid float, but it's possible that the float is
// out of range of valid values (which the lexer cannot determine).
// So mark the former as a bug but the latter as a legitimate user
// error.
//
// This is also true for integers.
if e, ok := err.(*strconv.NumError); ok &&
e.Err == strconv.ErrRange {
p.panicf("Float '%s' is out of the range of 64-bit "+
"IEEE-754 floating-point numbers.", it.val)
} else {
p.bug("Expected float value, but got '%s'.", it.val)
p.panicf("Invalid float value: %q", it.val)
}
}
return num, p.typeOfPrimitive(it)
case itemDatetime:
t, err := time.Parse("2006-01-02T15:04:05Z", it.val)
if err != nil {
p.bug("Expected Zulu formatted DateTime, but got '%s'.", it.val)
var t time.Time
var ok bool
var err error
for _, format := range []string{
"2006-01-02T15:04:05Z07:00",
"2006-01-02T15:04:05",
"2006-01-02",
} {
t, err = time.ParseInLocation(format, it.val, time.Local)
if err == nil {
ok = true
break
}
}
if !ok {
p.panicf("Invalid TOML Datetime: %q.", it.val)
}
return t, p.typeOfPrimitive(it)
case itemArray:
@@ -219,11 +269,75 @@ func (p *parser) value(it item) (interface{}, tomlType) {
types = append(types, typ)
}
return array, p.typeOfArray(types)
case itemInlineTableStart:
var (
hash = make(map[string]interface{})
outerContext = p.context
outerKey = p.currentKey
)
p.context = append(p.context, p.currentKey)
p.currentKey = ""
for it := p.next(); it.typ != itemInlineTableEnd; it = p.next() {
if it.typ != itemKeyStart {
p.bug("Expected key start but instead found %q, around line %d",
it.val, p.approxLine)
}
if it.typ == itemCommentStart {
p.expect(itemText)
continue
}
// retrieve key
k := p.next()
p.approxLine = k.line
kname := p.keyString(k)
// retrieve value
p.currentKey = kname
val, typ := p.value(p.next())
// make sure we keep metadata up to date
p.setType(kname, typ)
p.ordered = append(p.ordered, p.context.add(p.currentKey))
hash[kname] = val
}
p.context = outerContext
p.currentKey = outerKey
return hash, tomlHash
}
p.bug("Unexpected value type: %s", it.typ)
panic("unreachable")
}
// numUnderscoresOK checks whether each underscore in s is surrounded by
// characters that are not underscores.
func numUnderscoresOK(s string) bool {
accept := false
for _, r := range s {
if r == '_' {
if !accept {
return false
}
accept = false
continue
}
accept = true
}
return accept
}
// numPeriodsOK checks whether every period in s is followed by a digit.
func numPeriodsOK(s string) bool {
period := false
for _, r := range s {
if period && !isDigit(r) {
return false
}
period = r == '.'
}
return !period
}
// establishContext sets the current context of the parser,
// where the context is either a hash or an array of hashes. Which one is
// set depends on the value of the `array` parameter.
@@ -359,7 +473,8 @@ func (p *parser) addImplicit(key Key) {
p.implicits[key.String()] = true
}
// removeImplicit stops tagging the given key as having been implicitly created.
// removeImplicit stops tagging the given key as having been implicitly
// created.
func (p *parser) removeImplicit(key Key) {
p.implicits[key.String()] = false
}
@@ -381,64 +496,97 @@ func (p *parser) current() string {
return fmt.Sprintf("%s.%s", p.context, p.currentKey)
}
func replaceEscapes(s string) string {
return strings.NewReplacer(
"\\b", "\u0008",
"\\t", "\u0009",
"\\n", "\u000A",
"\\f", "\u000C",
"\\r", "\u000D",
"\\\"", "\u0022",
"\\/", "\u002F",
"\\\\", "\u005C",
).Replace(s)
}
func stripFirstNewline(s string) string {
if len(s) == 0 || s[0] != '\n' {
return s
}
return s[1:len(s)]
return s[1:]
}
func stripEscapedWhitespace(s string) string {
esc := strings.Split(s, "\\\n")
if len(esc) > 1 {
for i := 1; i < len(esc); i++ {
esc[i] = strings.TrimLeftFunc(esc[i], unicode.IsSpace)
}
}
return strings.Join(esc, "")
}
func (p *parser) replaceUnicode(s string) string {
indexEsc := func() int {
return strings.Index(s, "\\u")
func (p *parser) replaceEscapes(str string) string {
var replaced []rune
s := []byte(str)
r := 0
for r < len(s) {
if s[r] != '\\' {
c, size := utf8.DecodeRune(s[r:])
r += size
replaced = append(replaced, c)
continue
}
r += 1
if r >= len(s) {
p.bug("Escape sequence at end of string.")
return ""
}
switch s[r] {
default:
p.bug("Expected valid escape code after \\, but got %q.", s[r])
return ""
case 'b':
replaced = append(replaced, rune(0x0008))
r += 1
case 't':
replaced = append(replaced, rune(0x0009))
r += 1
case 'n':
replaced = append(replaced, rune(0x000A))
r += 1
case 'f':
replaced = append(replaced, rune(0x000C))
r += 1
case 'r':
replaced = append(replaced, rune(0x000D))
r += 1
case '"':
replaced = append(replaced, rune(0x0022))
r += 1
case '\\':
replaced = append(replaced, rune(0x005C))
r += 1
case 'u':
// At this point, we know we have a Unicode escape of the form
// `uXXXX` at [r, r+5). (Because the lexer guarantees this
// for us.)
escaped := p.asciiEscapeToUnicode(s[r+1 : r+5])
replaced = append(replaced, escaped)
r += 5
case 'U':
// At this point, we know we have a Unicode escape of the form
// `uXXXX` at [r, r+9). (Because the lexer guarantees this
// for us.)
escaped := p.asciiEscapeToUnicode(s[r+1 : r+9])
replaced = append(replaced, escaped)
r += 9
}
}
for i := indexEsc(); i != -1; i = indexEsc() {
asciiBytes := s[i+2 : i+6]
s = strings.Replace(s, s[i:i+6], p.asciiEscapeToUnicode(asciiBytes), -1)
}
return s
return string(replaced)
}
func (p *parser) asciiEscapeToUnicode(s string) string {
func (p *parser) asciiEscapeToUnicode(bs []byte) rune {
s := string(bs)
hex, err := strconv.ParseUint(strings.ToLower(s), 16, 32)
if err != nil {
p.bug("Could not parse '%s' as a hexadecimal number, but the "+
"lexer claims it's OK: %s", s, err)
}
// BUG(burntsushi)
// I honestly don't understand how this works. I can't seem
// to find a way to make this fail. I figured this would fail on invalid
// UTF-8 characters like U+DCFF, but it doesn't.
r := string(rune(hex))
if !utf8.ValidString(r) {
if !utf8.ValidRune(rune(hex)) {
p.panicf("Escaped character '\\u%s' is not valid UTF-8.", s)
}
return string(r)
return rune(hex)
}
func isStringType(ty itemType) bool {
return ty == itemString || ty == itemMultilineString ||
ty == itemRawString || ty == itemRawMultilineString
}
+2 -2
View File
@@ -83,8 +83,8 @@ func (p *parser) typeOfArray(types []tomlType) tomlType {
theType := types[0]
for _, t := range types[1:] {
if !typeEqual(theType, t) {
p.panicf("Array contains values of type '%s' and '%s', but arrays "+
"must be homogeneous.", theType, t)
p.panicf("Array contains values of type '%s' and '%s', but "+
"arrays must be homogeneous.", theType, t)
}
}
return tomlArray
+6 -5
View File
@@ -92,11 +92,11 @@ func typeFields(t reflect.Type) []field {
// Scan f.typ for fields to include.
for i := 0; i < f.typ.NumField(); i++ {
sf := f.typ.Field(i)
if sf.PkgPath != "" { // unexported
if sf.PkgPath != "" && !sf.Anonymous { // unexported
continue
}
name := sf.Tag.Get("toml")
if name == "-" {
opts := getOptions(sf.Tag)
if opts.skip {
continue
}
index := make([]int, len(f.index)+1)
@@ -110,8 +110,9 @@ func typeFields(t reflect.Type) []field {
}
// Record found field and index sequence.
if name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct {
tagged := name != ""
if opts.name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct {
tagged := opts.name != ""
name := opts.name
if name == "" {
name = sf.Name
}
+4 -2
View File
@@ -1,6 +1,8 @@
Copyright (c) 2012-2013 Dave Collins <dave@davec.name>
ISC License
Permission to use, copy, modify, and distribute this software for any
Copyright (c) 2012-2016 Dave Collins <dave@davec.name>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
+94 -100
View File
@@ -1,4 +1,4 @@
// Copyright (c) 2015 Dave Collins <dave@davec.name>
// Copyright (c) 2015-2016 Dave Collins <dave@davec.name>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -13,9 +13,12 @@
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// NOTE: Due to the following build constraints, this file will only be compiled
// when the code is not running on Google App Engine and "-tags disableunsafe"
// is not added to the go build command line.
// +build !appengine,!disableunsafe
// when the code is not running on Google App Engine, compiled by GopherJS, and
// "-tags safe" is not added to the go build command line. The "disableunsafe"
// tag is deprecated and thus should not be used.
// Go versions prior to 1.4 are disabled because they use a different layout
// for interfaces which make the implementation of unsafeReflectValue more complex.
// +build !js,!appengine,!safe,!disableunsafe,go1.4
package spew
@@ -33,80 +36,49 @@ const (
ptrSize = unsafe.Sizeof((*byte)(nil))
)
var (
// offsetPtr, offsetScalar, and offsetFlag are the offsets for the
// internal reflect.Value fields. These values are valid before golang
// commit ecccf07e7f9d which changed the format. The are also valid
// after commit 82f48826c6c7 which changed the format again to mirror
// the original format. Code in the init function updates these offsets
// as necessary.
offsetPtr = uintptr(ptrSize)
offsetScalar = uintptr(0)
offsetFlag = uintptr(ptrSize * 2)
type flag uintptr
// flagKindWidth and flagKindShift indicate various bits that the
// reflect package uses internally to track kind information.
//
// flagRO indicates whether or not the value field of a reflect.Value is
// read-only.
//
// flagIndir indicates whether the value field of a reflect.Value is
// the actual data or a pointer to the data.
//
// These values are valid before golang commit 90a7c3c86944 which
// changed their positions. Code in the init function updates these
// flags as necessary.
flagKindWidth = uintptr(5)
flagKindShift = uintptr(flagKindWidth - 1)
flagRO = uintptr(1 << 0)
flagIndir = uintptr(1 << 1)
var (
// flagRO indicates whether the value field of a reflect.Value
// is read-only.
flagRO flag
// flagAddr indicates whether the address of the reflect.Value's
// value may be taken.
flagAddr flag
)
func init() {
// Older versions of reflect.Value stored small integers directly in the
// ptr field (which is named val in the older versions). Versions
// between commits ecccf07e7f9d and 82f48826c6c7 added a new field named
// scalar for this purpose which unfortunately came before the flag
// field, so the offset of the flag field is different for those
// versions.
//
// This code constructs a new reflect.Value from a known small integer
// and checks if the size of the reflect.Value struct indicates it has
// the scalar field. When it does, the offsets are updated accordingly.
vv := reflect.ValueOf(0xf00)
if unsafe.Sizeof(vv) == (ptrSize * 4) {
offsetScalar = ptrSize * 2
offsetFlag = ptrSize * 3
}
// flagKindMask holds the bits that make up the kind
// part of the flags field. In all the supported versions,
// it is in the lower 5 bits.
const flagKindMask = flag(0x1f)
// Commit 90a7c3c86944 changed the flag positions such that the low
// order bits are the kind. This code extracts the kind from the flags
// field and ensures it's the correct type. When it's not, the flag
// order has been changed to the newer format, so the flags are updated
// accordingly.
upf := unsafe.Pointer(uintptr(unsafe.Pointer(&vv)) + offsetFlag)
upfv := *(*uintptr)(upf)
flagKindMask := uintptr((1<<flagKindWidth - 1) << flagKindShift)
if (upfv&flagKindMask)>>flagKindShift != uintptr(reflect.Int) {
flagKindShift = 0
flagRO = 1 << 5
flagIndir = 1 << 6
// Different versions of Go have used different
// bit layouts for the flags type. This table
// records the known combinations.
var okFlags = []struct {
ro, addr flag
}{{
// From Go 1.4 to 1.5
ro: 1 << 5,
addr: 1 << 7,
}, {
// Up to Go tip.
ro: 1<<5 | 1<<6,
addr: 1 << 8,
}}
// Commit adf9b30e5594 modified the flags to separate the
// flagRO flag into two bits which specifies whether or not the
// field is embedded. This causes flagIndir to move over a bit
// and means that flagRO is the combination of either of the
// original flagRO bit and the new bit.
//
// This code detects the change by extracting what used to be
// the indirect bit to ensure it's set. When it's not, the flag
// order has been changed to the newer format, so the flags are
// updated accordingly.
if upfv&flagIndir == 0 {
flagRO = 3 << 5
flagIndir = 1 << 7
}
var flagValOffset = func() uintptr {
field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
if !ok {
panic("reflect.Value has no flag field")
}
return field.Offset
}()
// flagField returns a pointer to the flag field of a reflect.Value.
func flagField(v *reflect.Value) *flag {
return (*flag)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + flagValOffset))
}
// unsafeReflectValue converts the passed reflect.Value into a one that bypasses
@@ -118,34 +90,56 @@ func init() {
// This allows us to check for implementations of the Stringer and error
// interfaces to be used for pretty printing ordinarily unaddressable and
// inaccessible values such as unexported struct fields.
func unsafeReflectValue(v reflect.Value) (rv reflect.Value) {
indirects := 1
vt := v.Type()
upv := unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetPtr)
rvf := *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetFlag))
if rvf&flagIndir != 0 {
vt = reflect.PtrTo(v.Type())
indirects++
} else if offsetScalar != 0 {
// The value is in the scalar field when it's not one of the
// reference types.
switch vt.Kind() {
case reflect.Uintptr:
case reflect.Chan:
case reflect.Func:
case reflect.Map:
case reflect.Ptr:
case reflect.UnsafePointer:
default:
upv = unsafe.Pointer(uintptr(unsafe.Pointer(&v)) +
offsetScalar)
func unsafeReflectValue(v reflect.Value) reflect.Value {
if !v.IsValid() || (v.CanInterface() && v.CanAddr()) {
return v
}
flagFieldPtr := flagField(&v)
*flagFieldPtr &^= flagRO
*flagFieldPtr |= flagAddr
return v
}
// Sanity checks against future reflect package changes
// to the type or semantics of the Value.flag field.
func init() {
field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
if !ok {
panic("reflect.Value has no flag field")
}
if field.Type.Kind() != reflect.TypeOf(flag(0)).Kind() {
panic("reflect.Value flag field has changed kind")
}
type t0 int
var t struct {
A t0
// t0 will have flagEmbedRO set.
t0
// a will have flagStickyRO set
a t0
}
vA := reflect.ValueOf(t).FieldByName("A")
va := reflect.ValueOf(t).FieldByName("a")
vt0 := reflect.ValueOf(t).FieldByName("t0")
// Infer flagRO from the difference between the flags
// for the (otherwise identical) fields in t.
flagPublic := *flagField(&vA)
flagWithRO := *flagField(&va) | *flagField(&vt0)
flagRO = flagPublic ^ flagWithRO
// Infer flagAddr from the difference between a value
// taken from a pointer and not.
vPtrA := reflect.ValueOf(&t).Elem().FieldByName("A")
flagNoPtr := *flagField(&vA)
flagPtr := *flagField(&vPtrA)
flagAddr = flagNoPtr ^ flagPtr
// Check that the inferred flags tally with one of the known versions.
for _, f := range okFlags {
if flagRO == f.ro && flagAddr == f.addr {
return
}
}
pv := reflect.NewAt(vt, upv)
rv = pv
for i := 0; i < indirects; i++ {
rv = rv.Elem()
}
return rv
panic("reflect.Value read-only flag has changed semantics")
}
+5 -4
View File
@@ -1,4 +1,4 @@
// Copyright (c) 2015 Dave Collins <dave@davec.name>
// Copyright (c) 2015-2016 Dave Collins <dave@davec.name>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -13,9 +13,10 @@
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// NOTE: Due to the following build constraints, this file will only be compiled
// when either the code is running on Google App Engine or "-tags disableunsafe"
// is added to the go build command line.
// +build appengine disableunsafe
// when the code is running on Google App Engine, compiled by GopherJS, or
// "-tags safe" is added to the go build command line. The "disableunsafe"
// tag is deprecated and thus should not be used.
// +build js appengine safe disableunsafe !go1.4
package spew
+2 -2
View File
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2013 Dave Collins <dave@davec.name>
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
@@ -180,7 +180,7 @@ func printComplex(w io.Writer, c complex128, floatPrecision int) {
w.Write(closeParenBytes)
}
// printHexPtr outputs a uintptr formatted as hexidecimal with a leading '0x'
// printHexPtr outputs a uintptr formatted as hexadecimal with a leading '0x'
// prefix to Writer w.
func printHexPtr(w io.Writer, p uintptr) {
// Null pointer.
-298
View File
@@ -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
View File
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2013 Dave Collins <dave@davec.name>
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
@@ -64,9 +64,18 @@ type ConfigState struct {
// inside these interface methods. As a result, this option relies on
// access to the unsafe package, so it will not have any effect when
// running in environments without access to the unsafe package such as
// Google App Engine or with the "disableunsafe" build tag specified.
// Google App Engine or with the "safe" build tag specified.
DisablePointerMethods bool
// DisablePointerAddresses specifies whether to disable the printing of
// pointer addresses. This is useful when diffing data structures in tests.
DisablePointerAddresses bool
// DisableCapacities specifies whether to disable the printing of capacities
// for arrays, slices, maps and channels. This is useful when diffing
// data structures in tests.
DisableCapacities bool
// ContinueOnMethod specifies whether or not recursion should continue once
// a custom error or Stringer interface is invoked. The default, false,
// means it will print the results of invoking the custom error or Stringer
+10 -1
View File
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2013 Dave Collins <dave@davec.name>
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
@@ -91,6 +91,15 @@ The following configuration options are available:
which only accept pointer receivers from non-pointer variables.
Pointer method invocation is enabled by default.
* DisablePointerAddresses
DisablePointerAddresses specifies whether to disable the printing of
pointer addresses. This is useful when diffing data structures in tests.
* DisableCapacities
DisableCapacities specifies whether to disable the printing of
capacities for arrays, slices, maps and channels. This is useful when
diffing data structures in tests.
* ContinueOnMethod
Enables recursion into types after invoking error and Stringer interface
methods. Recursion after method invocation is disabled by default.
+9 -9
View File
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2013 Dave Collins <dave@davec.name>
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
@@ -35,16 +35,16 @@ var (
// cCharRE is a regular expression that matches a cgo char.
// It is used to detect character arrays to hexdump them.
cCharRE = regexp.MustCompile("^.*\\._Ctype_char$")
cCharRE = regexp.MustCompile(`^.*\._Ctype_char$`)
// cUnsignedCharRE is a regular expression that matches a cgo unsigned
// char. It is used to detect unsigned character arrays to hexdump
// them.
cUnsignedCharRE = regexp.MustCompile("^.*\\._Ctype_unsignedchar$")
cUnsignedCharRE = regexp.MustCompile(`^.*\._Ctype_unsignedchar$`)
// cUint8tCharRE is a regular expression that matches a cgo uint8_t.
// It is used to detect uint8_t arrays to hexdump them.
cUint8tCharRE = regexp.MustCompile("^.*\\._Ctype_uint8_t$")
cUint8tCharRE = regexp.MustCompile(`^.*\._Ctype_uint8_t$`)
)
// dumpState contains information about the state of a dump operation.
@@ -129,7 +129,7 @@ func (d *dumpState) dumpPtr(v reflect.Value) {
d.w.Write(closeParenBytes)
// Display pointer information.
if len(pointerChain) > 0 {
if !d.cs.DisablePointerAddresses && len(pointerChain) > 0 {
d.w.Write(openParenBytes)
for i, addr := range pointerChain {
if i > 0 {
@@ -143,10 +143,10 @@ func (d *dumpState) dumpPtr(v reflect.Value) {
// Display dereferenced value.
d.w.Write(openParenBytes)
switch {
case nilFound == true:
case nilFound:
d.w.Write(nilAngleBytes)
case cycleFound == true:
case cycleFound:
d.w.Write(circularBytes)
default:
@@ -282,13 +282,13 @@ func (d *dumpState) dump(v reflect.Value) {
case reflect.Map, reflect.String:
valueLen = v.Len()
}
if valueLen != 0 || valueCap != 0 {
if valueLen != 0 || !d.cs.DisableCapacities && valueCap != 0 {
d.w.Write(openParenBytes)
if valueLen != 0 {
d.w.Write(lenEqualsBytes)
printInt(d.w, int64(valueLen), 10)
}
if valueCap != 0 {
if !d.cs.DisableCapacities && valueCap != 0 {
if valueLen != 0 {
d.w.Write(spaceBytes)
}
File diff suppressed because it is too large Load Diff
-98
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2013 Dave Collins <dave@davec.name>
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
@@ -182,10 +182,10 @@ func (f *formatState) formatPtr(v reflect.Value) {
// Display dereferenced value.
switch {
case nilFound == true:
case nilFound:
f.fs.Write(nilAngleBytes)
case cycleFound == true:
case cycleFound:
f.fs.Write(circularShortBytes)
default:
File diff suppressed because it is too large Load Diff
-87
View File
@@ -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
View File
@@ -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
View File
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2013 Dave Collins <dave@davec.name>
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
-309
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,58 @@
# Server-Sent Events
[![GoDoc](https://godoc.org/github.com/gin-contrib/sse?status.svg)](https://godoc.org/github.com/gin-contrib/sse)
[![Build Status](https://travis-ci.org/gin-contrib/sse.svg)](https://travis-ci.org/gin-contrib/sse)
[![codecov](https://codecov.io/gh/gin-contrib/sse/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-contrib/sse)
[![Go Report Card](https://goreportcard.com/badge/github.com/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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,2 +1,5 @@
Godeps/*
!Godeps/Godeps.json
vendor/*
!vendor/vendor.json
coverage.out
count.out
test
+32 -3
View File
@@ -1,6 +1,35 @@
language: go
sudo: false
go:
- 1.3
- 1.4
- tip
- 1.6.x
- 1.7.x
- 1.8.x
- 1.9.x
- 1.10.x
- master
git:
depth: 10
install:
- make install
go_import_path: github.com/gin-gonic/gin
script:
- make vet
- make fmt-check
- make embedmd
- make misspell-check
- make test
after_success:
- bash <(curl -s https://codecov.io/bash)
notifications:
webhooks:
urls:
- https://webhooks.gitter.im/e/7f95bf605c4d356372f4
on_success: change # options: [always|never|change] default: always
on_failure: always # options: [always|never|change] default: always
on_start: false # default: false
+61 -4
View File
@@ -1,14 +1,19 @@
List of all the awesome people working to make Gin the best Web Framework in Go.
## gin 1.x series authors
**Gin Core Team:** Bo-Yi Wu (@appleboy), 田欧 (@thinkerou), Javier Provecho (@javierprovecho)
##gin 0.x series authors
## gin 0.x series authors
**Original Developer:** Manu Martinez-Almeida (@manucorporat)
**Long-term Maintainer:** Javier Provecho (@javierprovecho)
**Maintainers:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho)
People and companies, who have contributed, in alphabetical order.
**@858806258 (杰哥)**
- Fix typo in example
**@achedeuzot (Klemen Sever)**
- Fix newline debug printing
@@ -21,6 +26,10 @@ People and companies, who have contributed, in alphabetical order.
- Typos in README
**@alexanderdidenko (Aleksandr Didenko)**
- Add support multipart/form-data
**@alexandernyquist (Alexander Nyquist)**
- Using template.Must to fix multiple return issue
- ★ Added support for OPTIONS verb
@@ -55,15 +64,39 @@ People and companies, who have contributed, in alphabetical order.
- Add example about serving static files
**@donileo (Adonis)**
- Add NoMethod handler
**@dutchcoders (DutchCoders)**
- ★ Fix security bug that allows client to spoof ip
- Fix typo. r.HTMLTemplates -> SetHTMLTemplate
**@el3ctro- (Joshua Loper)**
- Fix typo in example
**@ethankan (Ethan Kan)**
- Unsigned integers in binding
**(Evgeny Persienko)**
- Validate sub structures
**@frankbille (Frank Bille)**
- Add support for HTTP Realm Auth
**@fmd (Fareed Dudhia)**
- Fix typo. SetHTTPTemplate -> SetHTMLTemplate
**@ironiridis (Christopher Harrington)**
- Remove old reference
**@jammie-stackhouse (Jamie Stackhouse)**
- Add more shortcuts for router methods
@@ -104,6 +137,10 @@ People and companies, who have contributed, in alphabetical order.
- ★ work around path.Join removing trailing slashes from routes
**@mattn (Yasuhiro Matsumoto)**
- Improve color logger
**@mdigger (Dmitry Sedykh)**
- Fixes Form binding when content-type is x-www-form-urlencoded
- No repeat call c.Writer.Status() in gin.Logger
@@ -138,10 +175,22 @@ People and companies, who have contributed, in alphabetical order.
- Fix Port usage in README.
**@rayrod2030 (Ray Rodriguez)**
- Fix typo in example
**@rns**
- Fix typo in example
**@RobAWilkinson (Robert Wilkinson)**
- Add example of forms and params
**@rogierlommers (Rogier Lommers)**
- Add updated static serve example
**@se77en (Damon Zhao)**
- Improve color logging
@@ -166,9 +215,17 @@ People and companies, who have contributed, in alphabetical order.
- Update httprouter godeps
**@tebeka (Miki Tebeka)**
- Use net/http constants instead of numeric values
**@techjanitor**
- Update context.go reserved IPs
**@yosssi (Keiji Yoshida)**
- Fix link in README
**@yuyabee**
- Fixed README
- Fixed README

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