commit da7cacb57eb154cbfcbaeec38edb7894562e0b20 Author: didier.amyot Date: Wed May 7 21:47:51 2014 -0400 first commit. Way too late. diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..e500613 --- /dev/null +++ b/.hgignore @@ -0,0 +1,8 @@ +syntax: glob +re:^client/bower_components/ +re:^client/node_modules/ +re:^client/js/ +re:^client/lib/ +re:^vulture$ +*.komodoproject +*.out diff --git a/api.go b/api.go new file mode 100644 index 0000000..9f6a8ef --- /dev/null +++ b/api.go @@ -0,0 +1,140 @@ +package main + +import ( + "bitbucket.org/damyot/vulture/shared" + "errors" + "github.com/gorilla/mux" + "labix.org/v2/mgo" + "net/http" + "strconv" +) + +type VultureBackend struct { + Client *mgo.Session + Db *mgo.Database + Collection *mgo.Collection +} + +func GetVultureBackend(req *http.Request) (*VultureBackend, error) { + vars := mux.Vars(req) + serverURL, ok := vars["server"] + if !ok { + return nil, errors.New("server field not specified") + } + client, err := shared.GetMongoClient(serverURL) + if err != nil { + return nil, err + } + vb := &VultureBackend{Client: client} + vb.setDatabaseCollection(req) + return vb, nil +} + +func (this *VultureBackend) setDatabaseCollection(req *http.Request) { + vars := mux.Vars(req) + database, ok := vars["database"] + if !ok { + return + } + this.Db = this.Client.DB(database) + collection, ok := vars["collection"] + if !ok { + return + } + this.Collection = this.Db.C(collection) + +} + +func (this *VultureBackend) GetDataBases() ([]string, error) { + return this.Client.DatabaseNames() +} + +func (this *VultureBackend) GetCollections() ([]string, error) { + if this.Db == nil { + return nil, errors.New("no database set") + } + return this.Db.CollectionNames() +} + +func (this *VultureBackend) getIndexes() (interface{}, error) { + idxs, err := this.Collection.Indexes() + if err != nil { + return nil, err + } + ret := make([]map[string]interface{}, len(idxs)) + for i, idx := range idxs { + ret[i] = map[string]interface{}{"name": idx.Name, "keys": idx.Key} + } + return ret, nil + +} + +func (this *VultureBackend) getMetaData(query *mgo.Query) (map[string]interface{}, error) { + meta := make(map[string]interface{}) + var err error + if meta["count"], err = query.Count(); err != nil { + return meta, err + } + if meta["indexes"], err = this.getIndexes(); err != nil { + return meta, err + } + + return meta, nil + +} + +func (this *VultureBackend) GetDocumentByIndex(index int) (map[string]interface{}, error) { + doc := make(map[string]interface{}) + query := this.Collection.Find(nil) + if err := query.Skip(index).One(&doc); err != nil { + return nil, err + } + return this.wrapDocumentWithMetadata(doc, query) +} + +func (this *VultureBackend) wrapDocumentWithMetadata(doc map[string]interface{}, query *mgo.Query) (map[string]interface{}, error) { + ret := make(map[string]interface{}) + var err error + ret["document"] = doc + if ret["meta"], err = this.getMetaData(query); err != nil { + return ret, err + } + return ret, nil +} + +func getAvailableServers(w http.ResponseWriter, request *http.Request) (interface{}, error) { + return params.MongoServers, nil +} + +func getAvailableDataBases(w http.ResponseWriter, request *http.Request) (interface{}, error) { + vb, err := GetVultureBackend(request) + if err != nil { + return nil, err + } + return vb.GetDataBases() +} + +func getAvailableCollections(w http.ResponseWriter, request *http.Request) (interface{}, error) { + vb, err := GetVultureBackend(request) + if err != nil { + return nil, err + } + return vb.GetCollections() +} + +func getDocumentByIndex(w http.ResponseWriter, request *http.Request) (interface{}, error) { + vb, err := GetVultureBackend(request) + if err != nil { + return nil, err + } + vars := mux.Vars(request) + indexAsString, ok := vars["index"] + if !ok { + return nil, errors.New("index field not specified") + } + index, err := strconv.Atoi(indexAsString) + if err != nil { + return nil, errors.New("index field is not an inteder ( " + indexAsString + ")") + } + return vb.GetDocumentByIndex(index) +} diff --git a/client/Gruntfile.coffee b/client/Gruntfile.coffee new file mode 100644 index 0000000..478a3a2 --- /dev/null +++ b/client/Gruntfile.coffee @@ -0,0 +1,48 @@ +module.exports = (grunt) -> + + # Project configuration. + grunt.initConfig( + packagekg: grunt.file.readJSON('package.json') + bower: + install: {} + + connect: + server: + options: + port: 4506 + base: '.' + keepalive: true + + clean: ["js"] + + coffee: + compile: + expand: true + cwd: 'coffee' + src: ['**/*.coffee'] + dest: 'js' + ext: '.js' + + watch: + coffee: + files: ['**/*.coffee'] + tasks: ['coffee'] + options: + spawn: false + + concurrent: + target: ['watch', 'connect'], + + ) + + # Plugins + grunt.loadNpmTasks('grunt-bower-task') + grunt.loadNpmTasks('grunt-contrib-connect') + grunt.loadNpmTasks('grunt-contrib-coffee') + grunt.loadNpmTasks('grunt-contrib-watch') + grunt.loadNpmTasks('grunt-concurrent') + grunt.loadNpmTasks('grunt-contrib-clean') + + # tasks + grunt.registerTask('default', ['bower:install', 'clean', 'coffee:compile']) + grunt.registerTask('dev', ['default', 'concurrent:target']) diff --git a/client/bower.json b/client/bower.json new file mode 100644 index 0000000..a4d0d07 --- /dev/null +++ b/client/bower.json @@ -0,0 +1,21 @@ +{ + "name": "vulture", + "version": "0.1", + "authors": [ + "didier.amyot " + ], + "description": "MongoDB Browser", + "main": "index.html", + "license": "MIT", + "private": true, + "dependencies": { + "underscore": "~1.6.0", + "foundation": "~5.2.1", + "angular": "~1.2.14", + "angular-route": "~1.2.14", + "angular-local-storage": "~0.0.1", + "qunit": "~1.14.0", + "zepto": "~1.1.3", + "fontawesome": "~4.0.3" + } +} diff --git a/client/coffee/controllers/aboutCtrl.coffee b/client/coffee/controllers/aboutCtrl.coffee new file mode 100644 index 0000000..712cd5d --- /dev/null +++ b/client/coffee/controllers/aboutCtrl.coffee @@ -0,0 +1,5 @@ +root = this; + +root.controllers.controller('aboutCtrl', ['$scope', ($scope) -> + +]) \ No newline at end of file diff --git a/client/coffee/controllers/collectionsCtrl.coffee b/client/coffee/controllers/collectionsCtrl.coffee new file mode 100644 index 0000000..47446be --- /dev/null +++ b/client/coffee/controllers/collectionsCtrl.coffee @@ -0,0 +1,12 @@ +root = this; + + +root.controllers.controller('collectionsCtrl', ['$scope', '$routeParams', '$http', ($scope, $routeParams, $http) -> + $scope.server = $routeParams.server + $scope.database = $routeParams.database + $scope.collections = [] + $http.get("/api/#{ $routeParams.server }/#{ $routeParams.database }/collections").then((res) -> + $scope.collections = res.data + ) + +]) \ No newline at end of file diff --git a/client/coffee/controllers/databasesCtrl.coffee b/client/coffee/controllers/databasesCtrl.coffee new file mode 100644 index 0000000..4d17a9e --- /dev/null +++ b/client/coffee/controllers/databasesCtrl.coffee @@ -0,0 +1,11 @@ +root = this; + + +root.controllers.controller('databasesCtrl', ['$scope', '$routeParams', '$http', ($scope, $routeParams, $http) -> + $scope.server = $routeParams.server + $scope.databases = [] + $http.get("/api/#{ $routeParams.server }/databases").then((res) -> + $scope.databases = res.data + ) + +]) \ No newline at end of file diff --git a/client/coffee/controllers/documentCtrl.coffee b/client/coffee/controllers/documentCtrl.coffee new file mode 100644 index 0000000..20df927 --- /dev/null +++ b/client/coffee/controllers/documentCtrl.coffee @@ -0,0 +1,24 @@ +root = this; + + +root.controllers.controller('documentCtrl', ['$scope', '$routeParams', '$http', ($scope, $routeParams, $http) -> + $scope.idx = $routeParams.idx + $scope.doc = {} + $scope.idx = Number($routeParams.idx) + $scope.meta = {} + $http.get("/api/#{ $routeParams.server }/#{ $routeParams.database }/#{ $routeParams.collection }/idx/#{$routeParams.idx - 1}").then((res) -> + $scope.doc = res.data.document + $scope.meta = res.data.meta + ) + + $scope.previousDocumentUrl = () -> + "#/#{ $routeParams.server }/#{ $routeParams.database }/#{ $routeParams.collection }/idx/#{$scope.idx - 1}" + + $scope.nextDocumentUrl = () -> + "#/#{ $routeParams.server }/#{ $routeParams.database }/#{ $routeParams.collection }/idx/#{$scope.idx + 1}" + + $scope.hasPreviousDocument = () -> + $scope.idx > 1 + + +]) \ No newline at end of file diff --git a/client/coffee/controllers/homeCtrl.coffee b/client/coffee/controllers/homeCtrl.coffee new file mode 100644 index 0000000..5f909fd --- /dev/null +++ b/client/coffee/controllers/homeCtrl.coffee @@ -0,0 +1,7 @@ +root = this; + + +root.controllers.controller('homeCtrl', ['$scope', ($scope) -> + + +]) \ No newline at end of file diff --git a/client/coffee/controllers/serversCtrl.coffee b/client/coffee/controllers/serversCtrl.coffee new file mode 100644 index 0000000..34f0a7f --- /dev/null +++ b/client/coffee/controllers/serversCtrl.coffee @@ -0,0 +1,10 @@ +root = this; + + +root.controllers.controller('serversCtrl', ['$scope', '$http', ($scope, $http) -> + $scope.servers = [] + $http.get("/api/servers").then((res) -> + $scope.servers = res.data + ) + +]) \ No newline at end of file diff --git a/client/coffee/controllers/topBarCtrl.coffee b/client/coffee/controllers/topBarCtrl.coffee new file mode 100644 index 0000000..4fe1719 --- /dev/null +++ b/client/coffee/controllers/topBarCtrl.coffee @@ -0,0 +1,18 @@ +root = this; + +root.controllers.controller('topBarCtrl', ['$scope', '$location', ($scope, $location) -> + $scope.$on('$routeChangeSuccess', (next, current) -> + $scope.path = _.filter($location.path().split("/"), (s) -> s) + $scope.getHistoryUrl = (idx) -> + ret = "#/" + ($scope.path[i] for i in [0...idx]).join("/") + if $scope.path[idx] == "idx" + return "" + if idx == 0 + ret += "servers" + else if idx == 1 + ret += "/databases" + else if idx == 2 + ret += "/collections" + return ret + ) + ]) \ No newline at end of file diff --git a/client/coffee/directives/key.coffee b/client/coffee/directives/key.coffee new file mode 100644 index 0000000..8f235b2 --- /dev/null +++ b/client/coffee/directives/key.coffee @@ -0,0 +1,18 @@ + +directive = () -> + directive = + templateUrl: "/partials/directives/key.html" + replace: true, + transclude: false, + restrict: 'E', + scope: + value: "=" + controller:["$scope", ($scope) -> + $scope.hasIndexDown = false + $scope.hasIndexUp = false + $scope.hasIndex = false + $scope.has2DIndexUp = false + ] + return directive + +directives.directive( "vulKey", [directive]) diff --git a/client/coffee/directives/value.coffee b/client/coffee/directives/value.coffee new file mode 100644 index 0000000..f176065 --- /dev/null +++ b/client/coffee/directives/value.coffee @@ -0,0 +1,18 @@ + +directive = () -> + directive = + templateUrl: "/partials/directives/value.html" + replace: true, + transclude: false, + restrict: 'E', + scope: + value: "=" + controller:["$scope", ($scope) -> + $scope.isString = _.isString($scope.value) + $scope.isNumber = _.isNumber($scope.value) + $scope.isObject = _.isObject($scope.value) + $scope.isArray = _.isArray($scope.value) + ] + return directive + +directives.directive( "vulValue", [directive]) diff --git a/client/coffee/filters.coffee b/client/coffee/filters.coffee new file mode 100644 index 0000000..7571d2c --- /dev/null +++ b/client/coffee/filters.coffee @@ -0,0 +1,13 @@ +root = this; + +root.filters.filter 'escape', () -> + return window.escape; + + +root.filters.filter 'replaceAllCommasBySpaces', () -> + return (text) -> + return text.replace(new RegExp(",","g"), " ") + +root.filters.filter 'niceJSON', () -> + return (obj) -> + return JSON.stringify(obj, null, 4); diff --git a/client/coffee/vultureApp.coffee b/client/coffee/vultureApp.coffee new file mode 100644 index 0000000..98bdc15 --- /dev/null +++ b/client/coffee/vultureApp.coffee @@ -0,0 +1,25 @@ + +root = this; + +angular.module('vulture', [ + 'ngRoute' + 'vulture.filters' + 'vulture.services' + 'vulture.directives' + 'vulture.controllers' +]). +config(['$routeProvider', ($routeProvider) -> + $routeProvider.when('/servers', {templateUrl: 'partials/servers.html', controller: 'serversCtrl'}) + $routeProvider.when('/about', {templateUrl: 'partials/about.html', controller: 'aboutCtrl'}) + $routeProvider.when('/:server/databases', {templateUrl: 'partials/databases.html', controller: 'databasesCtrl'}) + $routeProvider.when('/:server/:database/collections', {templateUrl: 'partials/collections.html', controller: 'collectionsCtrl'}) + $routeProvider.when('/:server/:database/:collection/idx/:idx', {templateUrl: 'partials/document.html', controller: 'documentCtrl'}) + + $routeProvider.otherwise({redirectTo: '/servers'}) +]) + +root.filters = angular.module('vulture.filters', []) +root.services = angular.module('vulture.services', []) +root.directives = angular.module('vulture.directives', []) +root.controllers = angular.module('vulture.controllers', []) + diff --git a/client/css/RockSalt.ttf b/client/css/RockSalt.ttf new file mode 100644 index 0000000..88df5cd Binary files /dev/null and b/client/css/RockSalt.ttf differ diff --git a/client/css/vulture.css b/client/css/vulture.css new file mode 100644 index 0000000..3c69391 --- /dev/null +++ b/client/css/vulture.css @@ -0,0 +1,4 @@ +@font-face { + font-family: 'Rock Salt'; + src: URL('RockSalt.ttf') format ('ttf'); +} \ No newline at end of file diff --git a/client/images/gopher.svg b/client/images/gopher.svg new file mode 100644 index 0000000..171ecec --- /dev/null +++ b/client/images/gopher.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/vulture.svg b/client/images/vulture.svg new file mode 100644 index 0000000..4d7c55d --- /dev/null +++ b/client/images/vulture.svg @@ -0,0 +1,374 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..f8c666c --- /dev/null +++ b/client/index.html @@ -0,0 +1,64 @@ + + + + + + + vulture + + + + + + + + + + + + + + +
+
+

+ Vulture

+
+
+
+
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..ad059c5 --- /dev/null +++ b/client/package.json @@ -0,0 +1,11 @@ +{ + "devDependencies": { + "grunt": "^0.4.4", + "grunt-bower-task": "^0.3.4", + "grunt-contrib-connect": "^0.7.1", + "grunt-contrib-coffee": "^0.10.1", + "grunt-contrib-watch": "^0.6.1", + "grunt-concurrent": "^0.5.0", + "grunt-contrib-clean": "^0.5.0" + } +} diff --git a/client/partials/about.html b/client/partials/about.html new file mode 100644 index 0000000..1523aec --- /dev/null +++ b/client/partials/about.html @@ -0,0 +1,35 @@ +
+
+

+ Many thanks for using Vulture. For all questions, remarks, + bugs, please use bitbucket. +

+
+
+ +
+
+ +
+ +
+ +
+
+

+ This program could not have been possible without the + exceptionnal tools already available such as +

+ +
+ +
\ No newline at end of file diff --git a/client/partials/collections.html b/client/partials/collections.html new file mode 100644 index 0000000..d16023a --- /dev/null +++ b/client/partials/collections.html @@ -0,0 +1,17 @@ +
+
+

Choose your collection

+
+
+ + + + diff --git a/client/partials/databases.html b/client/partials/databases.html new file mode 100644 index 0000000..bcad9eb --- /dev/null +++ b/client/partials/databases.html @@ -0,0 +1,17 @@ +
+
+

Choose your database

+
+
+ + + + diff --git a/client/partials/directives/key.html b/client/partials/directives/key.html new file mode 100644 index 0000000..4f6f506 --- /dev/null +++ b/client/partials/directives/key.html @@ -0,0 +1,10 @@ + + {{value}} + + + + + + + + \ No newline at end of file diff --git a/client/partials/directives/value.html b/client/partials/directives/value.html new file mode 100644 index 0000000..802a83b --- /dev/null +++ b/client/partials/directives/value.html @@ -0,0 +1,12 @@ + + + {{ value}} + + + {{value | number | replaceAllCommasBySpaces}} + + +
{{value | niceJSON}}
+
+ +
\ No newline at end of file diff --git a/client/partials/document.html b/client/partials/document.html new file mode 100644 index 0000000..cc58ff3 --- /dev/null +++ b/client/partials/document.html @@ -0,0 +1,39 @@ +
+
+ +
+
+ +
+
+ +
+
+ + {{idx}} / {{ meta.count}} + +
+
+ +
+
+ + + + + + + + + + + + + +
KeysValues
+ +
+
diff --git a/client/partials/home.html b/client/partials/home.html new file mode 100644 index 0000000..281c686 --- /dev/null +++ b/client/partials/home.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/client/partials/servers.html b/client/partials/servers.html new file mode 100644 index 0000000..2b6e235 --- /dev/null +++ b/client/partials/servers.html @@ -0,0 +1,17 @@ +
+
+

Choose your server

+
+
+ + + + diff --git a/handlerWrapper.go b/handlerWrapper.go new file mode 100644 index 0000000..6cdd8f2 --- /dev/null +++ b/handlerWrapper.go @@ -0,0 +1,24 @@ +package main + +import ( + "encoding/json" + "net/http" +) + +type errorWrapper struct { + Error error `json:"error"` +} + +type ApiHandler func(w http.ResponseWriter, request *http.Request) (interface{}, error) + +func (this ApiHandler) ServeHTTP(w http.ResponseWriter, request *http.Request) { + ret, err := this(w, request) + if err != nil { + ret = &errorWrapper{err} + w.WriteHeader(500) + } else { + w.WriteHeader(200) + } + enc := json.NewEncoder(w) + enc.Encode(ret) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..a316164 --- /dev/null +++ b/main.go @@ -0,0 +1,89 @@ +package main + +import ( + "fmt" + "github.com/gorilla/mux" + "io/ioutil" + "launchpad.net/goyaml" + "log" + "net/http" + "path" + "strconv" +) + +const PARAM_FILE string = "params.yml" +const STATIC_DIR string = "client" + +type Params struct { + Port int `yaml:"Port,omitempty"` + ServingAddress string `yaml:"ServingAddress,omitempty"` + MongoServers []string `yaml:"MongoServers,omitempty"` +} + +func (this *Params) LoadFromYamlFile(fileName string) error { + data, err := ioutil.ReadFile(fileName) + if err != nil { + return err + } + return this.LoadFromYamlData(data) +} + +func (this *Params) LoadFromYamlData(data []byte) error { + var err error + p := new(Params) + if err = goyaml.Unmarshal(data, p); err != nil { + return err + } + + if p.ServingAddress != "" { + this.ServingAddress = p.ServingAddress + } + if p.Port > 0 { + this.Port = p.Port + } + if p.MongoServers != nil && len(p.MongoServers) > 0 { + this.MongoServers = p.MongoServers + } + return nil +} + +var params = &Params{8000, "localhost", []string{"localhost:27017"}} + +func init() { + if err := params.LoadFromYamlFile(PARAM_FILE); err != nil { + panic(err) + } +} + +func serveStatic(router *mux.Router) { + handler := func(w http.ResponseWriter, request *http.Request) { + vars := mux.Vars(request) + filepath := "/" + vars["path"] + w.Header().Set("Cache-Control", "public, max-age=43200") + http.ServeFile(w, request, path.Join(STATIC_DIR, filepath)) + } + router.HandleFunc("/{path:.*}", handler) +} + +func serveApi(router *mux.Router) { + router.Handle("/servers", ApiHandler(getAvailableServers)) + router.Handle("/{server}/databases", ApiHandler(getAvailableDataBases)) + router.Handle("/{server}/{database}/collections", ApiHandler(getAvailableCollections)) + router.Handle("/{server}/{database}/{collection}/idx/{index:[0-9]+}", ApiHandler(getDocumentByIndex)) + +} + +func createMuxRouter() http.Handler { + r := mux.NewRouter() + serveApi(r.PathPrefix("/api").Subrouter()) + serveStatic(r.PathPrefix("/").Subrouter()) + return r +} + +func main() { + address := params.ServingAddress + ":" + strconv.Itoa(params.Port) + fmt.Println("serving ", address) + if err := http.ListenAndServe(address, createMuxRouter()); err != nil { + log.Fatal(err) + } +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..0d1b2f0 --- /dev/null +++ b/main_test.go @@ -0,0 +1,60 @@ +package main + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "path" + "reflect" + "testing" +) + +func TestLoadFromYamlDataAndInitialParams(t *testing.T) { + if params.Port != 8000 { + t.Error("wrong port") + } + if params.ServingAddress != "localhost" { + t.Error("wrong png dpi") + } + if !reflect.DeepEqual(params.MongoServers, []string{"localhost:27017"}) { + t.Fail() + } +} + +func createMockStaticFile() (*os.File, error) { + f, err := ioutil.TempFile(STATIC_DIR, "go_testing") + if err != nil { + return nil, err + } + return f, ioutil.WriteFile(f.Name(), []byte("hello world"), 0600) + +} + +func TestServingStaticFiles(t *testing.T) { + ts := httptest.NewServer(createMuxRouter()) + defer ts.Close() + + f, err := createMockStaticFile() + if err != nil { + t.Error(err) + return + } + defer os.Remove(f.Name()) + + res, err := http.Get(ts.URL + "/" + path.Base(f.Name())) + if err != nil { + t.Error(err) + return + } + body, err := ioutil.ReadAll(res.Body) + res.Body.Close() + if err != nil { + t.Error(err) + return + } + if string(body) != "hello world" { + t.Error(f.Name()) + } + +} diff --git a/params.yml b/params.yml new file mode 100644 index 0000000..24bf5f3 --- /dev/null +++ b/params.yml @@ -0,0 +1,16 @@ +--- + +# This is a YAML configuration file. To see yaml file specification, see its +# wikipedia page at http://en.wikipedia.org/wiki/YAML + + +# The port where the service will run +Port: 8000 + +# The address where the service will run. If you wish to run it on your computer, you +# should use `localhost`. If you wish to share the service on your local network, you +# should use `0.0.0.0` +ServingAddress: localhost + +MongoServers: + - localhost:27017 \ No newline at end of file diff --git a/shared/mongo.go b/shared/mongo.go new file mode 100644 index 0000000..65a8133 --- /dev/null +++ b/shared/mongo.go @@ -0,0 +1,101 @@ +package shared + +import ( + "errors" + "fmt" + "labix.org/v2/mgo" + "net/url" + "strings" +) + +var mongoClients = make(map[string]*mgo.Session) + +func GetCanonicalMongoURL(u string) (string, error) { + urlinfo, err := parseMongoURL(u) + if err != nil { + return "", err + } + return strings.Join(urlinfo.addrs, "/"), nil + +} + +func GetMongoClient(url string) (*mgo.Session, error) { + murl, err := GetCanonicalMongoURL(url) + if err != nil { + return nil, err + } + client, ok := mongoClients[murl] + if !ok { + return getNewMongoClient(url) + } + if client.Ping() != nil { + return getNewMongoClient(url) + } + return client, nil +} + +func getNewMongoClient(url string) (*mgo.Session, error) { + client, err := mgo.Dial(url) + if err != nil { + return nil, err + } + murl, err := GetCanonicalMongoURL(url) + if err != nil { + return nil, err + } + mongoClients[murl] = client + return client, nil +} + +type urlInfo struct { + addrs []string + user string + pass string + db string + options map[string]string +} + +func isOptSep(c rune) bool { + return c == ';' || c == '&' +} + +func parseMongoURL(s string) (*urlInfo, error) { + if strings.HasPrefix(s, "mongodb://") { + s = s[10:] + } + info := &urlInfo{options: make(map[string]string)} + if c := strings.Index(s, "?"); c != -1 { + for _, pair := range strings.FieldsFunc(s[c+1:], isOptSep) { + l := strings.SplitN(pair, "=", 2) + if len(l) != 2 || l[0] == "" || l[1] == "" { + return nil, errors.New("connection option must be key=value: " + pair) + } + info.options[l[0]] = l[1] + } + s = s[:c] + } + if c := strings.Index(s, "@"); c != -1 { + pair := strings.SplitN(s[:c], ":", 2) + if len(pair) > 2 || pair[0] == "" { + return nil, errors.New("credentials must be provided as user:pass@host") + } + var err error + info.user, err = url.QueryUnescape(pair[0]) + if err != nil { + return nil, fmt.Errorf("cannot unescape username in URL: %q", pair[0]) + } + if len(pair) > 1 { + info.pass, err = url.QueryUnescape(pair[1]) + if err != nil { + return nil, fmt.Errorf("cannot unescape password in URL") + } + } + s = s[c+1:] + } + if c := strings.Index(s, "/"); c != -1 { + info.db = s[c+1:] + s = s[:c] + } + info.addrs = strings.Split(s, ",") + return info, nil +} diff --git a/shared/mongo_test.go b/shared/mongo_test.go new file mode 100644 index 0000000..43d9346 --- /dev/null +++ b/shared/mongo_test.go @@ -0,0 +1,18 @@ +package shared + +import ( + "testing" +) + +func TestGetCanonicalMongoURL(t *testing.T) { + tests := map[string]string{"localhost:8000": "localhost:8000", + "mongodb://myuser:mypass@localhost:40001/mydb": "localhost:40001", + "localhost": "localhost"} + for u, exp := range tests { + if s, err := GetCanonicalMongoURL(u); err != nil { + t.Error(err) + } else if s != exp { + t.Error(s, " != ", exp) + } + } +}