Restructure application
This commit is contained in:
362
pkg/api/api.go
Normal file
362
pkg/api/api.go
Normal file
@@ -0,0 +1,362 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"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/data"
|
||||
)
|
||||
|
||||
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",
|
||||
}
|
||||
|
||||
var DbClient *client.Client
|
||||
|
||||
type Error struct {
|
||||
Message string `json:"error"`
|
||||
}
|
||||
|
||||
func NewError(err error) Error {
|
||||
return Error{err.Error()}
|
||||
}
|
||||
|
||||
func assetContentType(name string) string {
|
||||
ext := filepath.Ext(name)
|
||||
result := mime.TypeByExtension(ext)
|
||||
|
||||
if result == "" {
|
||||
result = extraMimeTypes[ext]
|
||||
}
|
||||
|
||||
if result == "" {
|
||||
result = "text/plain; charset=utf-8"
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func SetupRoutes(router *gin.Engine) {
|
||||
router.GET("/", API_Home)
|
||||
router.GET("/static/*path", API_ServeAsset)
|
||||
|
||||
api := router.Group("/api")
|
||||
{
|
||||
api.Use(ApiMiddleware())
|
||||
|
||||
api.POST("/connect", API_Connect)
|
||||
api.GET("/databases", API_GetDatabases)
|
||||
api.GET("/connection", API_ConnectionInfo)
|
||||
api.GET("/activity", API_Activity)
|
||||
api.GET("/schemas", API_GetSchemas)
|
||||
api.GET("/tables", API_GetTables)
|
||||
api.GET("/tables/:table", API_GetTable)
|
||||
api.GET("/tables/:table/rows", API_GetTableRows)
|
||||
api.GET("/tables/:table/info", API_GetTableInfo)
|
||||
api.GET("/tables/:table/indexes", API_TableIndexes)
|
||||
api.GET("/query", API_RunQuery)
|
||||
api.POST("/query", API_RunQuery)
|
||||
api.GET("/explain", API_ExplainQuery)
|
||||
api.POST("/explain", API_ExplainQuery)
|
||||
api.GET("/history", API_History)
|
||||
api.GET("/bookmarks", API_Bookmarks)
|
||||
}
|
||||
}
|
||||
|
||||
// Middleware function to check database connection status before running queries
|
||||
func ApiMiddleware() gin.HandlerFunc {
|
||||
allowedPaths := []string{
|
||||
"/api/connect",
|
||||
"/api/bookmarks",
|
||||
"/api/history",
|
||||
}
|
||||
|
||||
return func(c *gin.Context) {
|
||||
if DbClient != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
currentPath := c.Request.URL.Path
|
||||
allowed := false
|
||||
|
||||
for _, path := range allowedPaths {
|
||||
if path == currentPath {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allowed {
|
||||
c.Next()
|
||||
} else {
|
||||
c.JSON(400, Error{"Not connected"})
|
||||
c.Abort()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func API_Home(c *gin.Context) {
|
||||
data, err := data.Asset("static/index.html")
|
||||
|
||||
if err != nil {
|
||||
c.String(400, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data(200, "text/html; charset=utf-8", data)
|
||||
}
|
||||
|
||||
func API_Connect(c *gin.Context) {
|
||||
url := c.Request.FormValue("url")
|
||||
|
||||
if url == "" {
|
||||
c.JSON(400, Error{"Url parameter is required"})
|
||||
return
|
||||
}
|
||||
|
||||
opts := command.Options{Url: url}
|
||||
url, err := connection.FormatUrl(opts)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, Error{err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
cl, err := client.NewFromUrl(url)
|
||||
if err != nil {
|
||||
c.JSON(400, Error{err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
err = cl.Test()
|
||||
if err != nil {
|
||||
c.JSON(400, Error{err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
info, err := cl.Info()
|
||||
|
||||
if err == nil {
|
||||
if DbClient != nil {
|
||||
DbClient.Close()
|
||||
}
|
||||
|
||||
DbClient = cl
|
||||
}
|
||||
|
||||
c.JSON(200, info.Format()[0])
|
||||
}
|
||||
|
||||
func API_GetDatabases(c *gin.Context) {
|
||||
names, err := DbClient.Databases()
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, names)
|
||||
}
|
||||
|
||||
func API_RunQuery(c *gin.Context) {
|
||||
query := strings.TrimSpace(c.Request.FormValue("query"))
|
||||
|
||||
if query == "" {
|
||||
c.JSON(400, errors.New("Query parameter is missing"))
|
||||
return
|
||||
}
|
||||
|
||||
API_HandleQuery(query, c)
|
||||
}
|
||||
|
||||
func API_ExplainQuery(c *gin.Context) {
|
||||
query := strings.TrimSpace(c.Request.FormValue("query"))
|
||||
|
||||
if query == "" {
|
||||
c.JSON(400, errors.New("Query parameter is missing"))
|
||||
return
|
||||
}
|
||||
|
||||
API_HandleQuery(fmt.Sprintf("EXPLAIN ANALYZE %s", query), c)
|
||||
}
|
||||
|
||||
func API_GetSchemas(c *gin.Context) {
|
||||
names, err := DbClient.Schemas()
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, names)
|
||||
}
|
||||
|
||||
func API_GetTables(c *gin.Context) {
|
||||
names, err := DbClient.Tables()
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, names)
|
||||
}
|
||||
|
||||
func API_GetTable(c *gin.Context) {
|
||||
res, err := DbClient.Table(c.Params.ByName("table"))
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, res)
|
||||
}
|
||||
|
||||
func API_GetTableRows(c *gin.Context) {
|
||||
limit := 1000 // Number of rows to fetch
|
||||
limitVal := c.Request.FormValue("limit")
|
||||
|
||||
if limitVal != "" {
|
||||
num, err := strconv.Atoi(limitVal)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, Error{"Invalid limit value"})
|
||||
return
|
||||
}
|
||||
|
||||
if num <= 0 {
|
||||
c.JSON(400, Error{"Limit should be greater than 0"})
|
||||
return
|
||||
}
|
||||
|
||||
limit = num
|
||||
}
|
||||
|
||||
opts := client.RowsOptions{
|
||||
Limit: limit,
|
||||
SortColumn: c.Request.FormValue("sort_column"),
|
||||
SortOrder: c.Request.FormValue("sort_order"),
|
||||
}
|
||||
|
||||
res, err := DbClient.TableRows(c.Params.ByName("table"), opts)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, res)
|
||||
}
|
||||
|
||||
func API_GetTableInfo(c *gin.Context) {
|
||||
res, err := DbClient.TableInfo(c.Params.ByName("table"))
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, res.Format()[0])
|
||||
}
|
||||
|
||||
func API_History(c *gin.Context) {
|
||||
c.JSON(200, DbClient.History)
|
||||
}
|
||||
|
||||
func API_ConnectionInfo(c *gin.Context) {
|
||||
res, err := DbClient.Info()
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, res.Format()[0])
|
||||
}
|
||||
|
||||
func API_Activity(c *gin.Context) {
|
||||
res, err := DbClient.Activity()
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, res)
|
||||
}
|
||||
|
||||
func API_TableIndexes(c *gin.Context) {
|
||||
res, err := DbClient.TableIndexes(c.Params.ByName("table"))
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, res)
|
||||
}
|
||||
|
||||
func API_HandleQuery(query string, c *gin.Context) {
|
||||
result, err := DbClient.Query(query)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
q := c.Request.URL.Query()
|
||||
|
||||
if len(q["format"]) > 0 && q["format"][0] == "csv" {
|
||||
filename := fmt.Sprintf("pgweb-%v.csv", time.Now().Unix())
|
||||
c.Writer.Header().Set("Content-disposition", "attachment;filename="+filename)
|
||||
c.Data(200, "text/csv", result.CSV())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, result)
|
||||
}
|
||||
|
||||
func API_Bookmarks(c *gin.Context) {
|
||||
bookmarks, err := bookmarks.ReadAll(bookmarks.Path())
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, NewError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, bookmarks)
|
||||
}
|
||||
|
||||
func API_ServeAsset(c *gin.Context) {
|
||||
path := "static" + c.Params.ByName("path")
|
||||
data, err := data.Asset(path)
|
||||
|
||||
if err != nil {
|
||||
c.String(400, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
c.String(404, "Asset is empty")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data(200, assetContentType(path), data)
|
||||
}
|
||||
29
pkg/api/api_test.go
Normal file
29
pkg/api/api_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_assetContentType(t *testing.T) {
|
||||
samples := map[string]string{
|
||||
"foo.html": "text/html; charset=utf-8",
|
||||
"foo.css": "text/css; charset=utf-8",
|
||||
"foo.js": "application/javascript",
|
||||
"foo.icon": "image-x-icon",
|
||||
"foo.png": "image/png",
|
||||
"foo.jpg": "image/jpeg",
|
||||
"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",
|
||||
}
|
||||
|
||||
for name, expected := range samples {
|
||||
assert.Equal(t, expected, assetContentType(name))
|
||||
}
|
||||
}
|
||||
71
pkg/bookmarks/bookmarks.go
Normal file
71
pkg/bookmarks/bookmarks.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package bookmarks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func readServerConfig(path string) (Bookmark, error) {
|
||||
bookmark := Bookmark{}
|
||||
|
||||
buff, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return bookmark, err
|
||||
}
|
||||
|
||||
_, err = toml.Decode(string(buff), &bookmark)
|
||||
return bookmark, err
|
||||
}
|
||||
|
||||
func fileBasename(path string) string {
|
||||
filename := filepath.Base(path)
|
||||
return strings.Replace(filename, filepath.Ext(path), "", 1)
|
||||
}
|
||||
|
||||
func Path() string {
|
||||
path, _ := homedir.Dir()
|
||||
return fmt.Sprintf("%s/.pgweb/bookmarks", path)
|
||||
}
|
||||
|
||||
func ReadAll(path string) (map[string]Bookmark, error) {
|
||||
results := map[string]Bookmark{}
|
||||
|
||||
files, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
return results, err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if filepath.Ext(file.Name()) != ".toml" {
|
||||
continue
|
||||
}
|
||||
|
||||
fullPath := path + "/" + file.Name()
|
||||
key := fileBasename(file.Name())
|
||||
config, err := readServerConfig(fullPath)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("%s parse error: %s\n", fullPath, err)
|
||||
continue
|
||||
}
|
||||
|
||||
results[key] = config
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
71
pkg/bookmarks/bookmarks_test.go
Normal file
71
pkg/bookmarks/bookmarks_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package bookmarks
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Invalid_Bookmark_Files(t *testing.T) {
|
||||
_, err := readServerConfig("foobar")
|
||||
assert.Error(t, err)
|
||||
|
||||
_, 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 'main.Bookmark.Port': Expected string but found 'int64'.", 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, "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)
|
||||
}
|
||||
|
||||
func Test_Bookmark_URL(t *testing.T) {
|
||||
bookmark, err := readServerConfig("./data/bookmark_url.toml")
|
||||
|
||||
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, "", bookmark.User)
|
||||
assert.Equal(t, "", bookmark.Database)
|
||||
assert.Equal(t, "", bookmark.Ssl)
|
||||
assert.Equal(t, "", bookmark.Password)
|
||||
}
|
||||
|
||||
func Test_Bookmarks_Path(t *testing.T) {
|
||||
assert.NotEqual(t, "/.pgweb/bookmarks", bookmarksPath())
|
||||
}
|
||||
|
||||
func Test_Basename(t *testing.T) {
|
||||
assert.Equal(t, "filename", fileBasename("filename.toml"))
|
||||
assert.Equal(t, "filename", fileBasename("path/filename.toml"))
|
||||
assert.Equal(t, "filename", fileBasename("~/long/path/filename.toml"))
|
||||
assert.Equal(t, "filename", fileBasename("filename"))
|
||||
}
|
||||
|
||||
func Test_ReadBookmarks_Invalid(t *testing.T) {
|
||||
bookmarks, err := readAllBookmarks("foobar")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 0, len(bookmarks))
|
||||
}
|
||||
|
||||
func Test_ReadBookmarks(t *testing.T) {
|
||||
bookmarks, err := readAllBookmarks("./data")
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, 2, len(bookmarks))
|
||||
}
|
||||
259
pkg/client/client.go
Normal file
259
pkg/client/client.go
Normal file
@@ -0,0 +1,259 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/sosedoff/pgweb/pkg/command"
|
||||
"github.com/sosedoff/pgweb/pkg/connection"
|
||||
"github.com/sosedoff/pgweb/pkg/history"
|
||||
"github.com/sosedoff/pgweb/pkg/statements"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
db *sqlx.DB
|
||||
History []history.Record
|
||||
ConnectionString string
|
||||
}
|
||||
|
||||
type Row []interface{}
|
||||
|
||||
type Result struct {
|
||||
Columns []string `json:"columns"`
|
||||
Rows []Row `json:"rows"`
|
||||
}
|
||||
|
||||
// Struct to hold table rows browsing options
|
||||
type RowsOptions struct {
|
||||
Limit int // Number of rows to fetch
|
||||
SortColumn string // Column to sort by
|
||||
SortOrder string // Sort direction (ASC, DESC)
|
||||
}
|
||||
|
||||
func New() (*Client, error) {
|
||||
str, err := connection.BuildString(command.Opts)
|
||||
|
||||
if command.Opts.Debug && str != "" {
|
||||
fmt.Println("Creating a new client for:", str)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db, err := sqlx.Open("postgres", str)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := Client{
|
||||
db: db,
|
||||
ConnectionString: str,
|
||||
History: history.New(),
|
||||
}
|
||||
|
||||
return &client, nil
|
||||
}
|
||||
|
||||
func NewFromUrl(url string) (*Client, error) {
|
||||
if command.Opts.Debug {
|
||||
fmt.Println("Creating a new client for:", url)
|
||||
}
|
||||
|
||||
db, err := sqlx.Open("postgres", url)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := Client{
|
||||
db: db,
|
||||
ConnectionString: url,
|
||||
History: history.New(),
|
||||
}
|
||||
|
||||
return &client, nil
|
||||
}
|
||||
|
||||
func (client *Client) Test() error {
|
||||
return client.db.Ping()
|
||||
}
|
||||
|
||||
func (client *Client) Info() (*Result, error) {
|
||||
return client.query(statements.PG_INFO)
|
||||
}
|
||||
|
||||
func (client *Client) Databases() ([]string, error) {
|
||||
return client.fetchRows(statements.PG_DATABASES)
|
||||
}
|
||||
|
||||
func (client *Client) Schemas() ([]string, error) {
|
||||
return client.fetchRows(statements.PG_SCHEMAS)
|
||||
}
|
||||
|
||||
func (client *Client) Tables() ([]string, error) {
|
||||
return client.fetchRows(statements.PG_TABLES)
|
||||
}
|
||||
|
||||
func (client *Client) Table(table string) (*Result, error) {
|
||||
return client.query(statements.PG_TABLE_SCHEMA, table)
|
||||
}
|
||||
|
||||
func (client *Client) TableRows(table string, opts RowsOptions) (*Result, error) {
|
||||
sql := fmt.Sprintf(`SELECT * FROM "%s"`, table)
|
||||
|
||||
if opts.SortColumn != "" {
|
||||
if opts.SortOrder == "" {
|
||||
opts.SortOrder = "ASC"
|
||||
}
|
||||
|
||||
sql += fmt.Sprintf(" ORDER BY %s %s", opts.SortColumn, opts.SortOrder)
|
||||
}
|
||||
|
||||
if opts.Limit > 0 {
|
||||
sql += fmt.Sprintf(" LIMIT %d", opts.Limit)
|
||||
}
|
||||
|
||||
return client.query(sql)
|
||||
}
|
||||
|
||||
func (client *Client) TableInfo(table string) (*Result, error) {
|
||||
return client.query(statements.PG_TABLE_INFO, table)
|
||||
}
|
||||
|
||||
func (client *Client) TableIndexes(table string) (*Result, error) {
|
||||
res, err := client.query(statements.PG_TABLE_INDEXES, table)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Returns all active queriers on the server
|
||||
func (client *Client) Activity() (*Result, error) {
|
||||
return client.query(statements.PG_ACTIVITY)
|
||||
}
|
||||
|
||||
func (client *Client) Query(query string) (*Result, error) {
|
||||
res, err := client.query(query)
|
||||
|
||||
// Save history records only if query did not fail
|
||||
if err == nil {
|
||||
client.History = append(client.History, history.NewRecord(query))
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (client *Client) query(query string, args ...interface{}) (*Result, error) {
|
||||
rows, err := client.db.Queryx(query, args...)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
cols, err := rows.Columns()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := Result{Columns: cols}
|
||||
|
||||
for rows.Next() {
|
||||
obj, err := rows.SliceScan()
|
||||
|
||||
for i, item := range obj {
|
||||
if item == nil {
|
||||
obj[i] = nil
|
||||
} else {
|
||||
t := reflect.TypeOf(item).Kind().String()
|
||||
|
||||
if t == "slice" {
|
||||
obj[i] = string(item.([]byte))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
result.Rows = append(result.Rows, obj)
|
||||
}
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (res *Result) Format() []map[string]interface{} {
|
||||
var items []map[string]interface{}
|
||||
|
||||
for _, row := range res.Rows {
|
||||
item := make(map[string]interface{})
|
||||
|
||||
for i, c := range res.Columns {
|
||||
item[c] = row[i]
|
||||
}
|
||||
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func (res *Result) CSV() []byte {
|
||||
buff := &bytes.Buffer{}
|
||||
writer := csv.NewWriter(buff)
|
||||
|
||||
writer.Write(res.Columns)
|
||||
|
||||
for _, row := range res.Rows {
|
||||
record := make([]string, len(res.Columns))
|
||||
|
||||
for i, item := range row {
|
||||
if item != nil {
|
||||
record[i] = fmt.Sprintf("%v", item)
|
||||
} else {
|
||||
record[i] = ""
|
||||
}
|
||||
}
|
||||
|
||||
err := writer.Write(record)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
writer.Flush()
|
||||
return buff.Bytes()
|
||||
}
|
||||
|
||||
// Close database connection
|
||||
func (client *Client) Close() error {
|
||||
return client.db.Close()
|
||||
}
|
||||
|
||||
// Fetch all rows as strings for a single column
|
||||
func (client *Client) fetchRows(q string) ([]string, error) {
|
||||
res, err := client.query(q)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Init empty slice so json.Marshal will encode it to "[]" instead of "null"
|
||||
results := make([]string, 0)
|
||||
|
||||
for _, row := range res.Rows {
|
||||
results = append(results, row[0].(string))
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
255
pkg/client/client_test.go
Normal file
255
pkg/client/client_test.go
Normal file
@@ -0,0 +1,255 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var testClient *Client
|
||||
var testCommands map[string]string
|
||||
|
||||
func setupCommands() {
|
||||
testCommands = map[string]string{
|
||||
"createdb": "createdb",
|
||||
"psql": "psql",
|
||||
"dropdb": "dropdb",
|
||||
}
|
||||
|
||||
if onWindows() {
|
||||
for k, v := range testCommands {
|
||||
testCommands[k] = v + ".exe"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func onWindows() bool {
|
||||
return runtime.GOOS == "windows"
|
||||
}
|
||||
|
||||
func setup() {
|
||||
out, err := exec.Command(testCommands["createdb"], "-U", "postgres", "-h", "localhost", "booktown").CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Database creation failed:", string(out))
|
||||
fmt.Println("Error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
out, err = exec.Command(testCommands["psql"], "-U", "postgres", "-h", "localhost", "-f", "./data/booktown.sql", "booktown").CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Database import failed:", string(out))
|
||||
fmt.Println("Error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func setupClient() {
|
||||
testClient, _ = NewClientFromUrl("postgres://postgres@localhost/booktown?sslmode=disable")
|
||||
}
|
||||
|
||||
func teardownClient() {
|
||||
if testClient != nil {
|
||||
testClient.db.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func teardown() {
|
||||
_, err := exec.Command(testCommands["dropdb"], "-U", "postgres", "-h", "localhost", "booktown").CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Teardown error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func test_NewClientFromUrl(t *testing.T) {
|
||||
url := "postgres://postgres@localhost/booktown?sslmode=disable"
|
||||
client, err := NewClientFromUrl(url)
|
||||
|
||||
if err != nil {
|
||||
defer client.db.Close()
|
||||
}
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, url, client.connectionString)
|
||||
}
|
||||
|
||||
func test_Test(t *testing.T) {
|
||||
assert.Equal(t, nil, testClient.Test())
|
||||
}
|
||||
|
||||
func test_Info(t *testing.T) {
|
||||
res, err := testClient.Info()
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.NotEqual(t, nil, res)
|
||||
}
|
||||
|
||||
func test_Databases(t *testing.T) {
|
||||
res, err := testClient.Databases()
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Contains(t, res, "booktown")
|
||||
assert.Contains(t, res, "postgres")
|
||||
}
|
||||
|
||||
func test_Tables(t *testing.T) {
|
||||
res, err := testClient.Tables()
|
||||
|
||||
expected := []string{
|
||||
"alternate_stock",
|
||||
"authors",
|
||||
"book_backup",
|
||||
"book_queue",
|
||||
"books",
|
||||
"customers",
|
||||
"daily_inventory",
|
||||
"distinguished_authors",
|
||||
"editions",
|
||||
"employees",
|
||||
"favorite_authors",
|
||||
"favorite_books",
|
||||
"money_example",
|
||||
"my_list",
|
||||
"numeric_values",
|
||||
"publishers",
|
||||
"recent_shipments",
|
||||
"schedules",
|
||||
"shipments",
|
||||
"states",
|
||||
"stock",
|
||||
"stock_backup",
|
||||
"stock_view",
|
||||
"subjects",
|
||||
"text_sorting",
|
||||
}
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, expected, res)
|
||||
}
|
||||
|
||||
func test_Table(t *testing.T) {
|
||||
res, err := testClient.Table("books")
|
||||
|
||||
columns := []string{
|
||||
"column_name",
|
||||
"data_type",
|
||||
"is_nullable",
|
||||
"character_maximum_length",
|
||||
"character_set_catalog",
|
||||
"column_default",
|
||||
}
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, columns, res.Columns)
|
||||
assert.Equal(t, 4, len(res.Rows))
|
||||
}
|
||||
|
||||
func test_TableRows(t *testing.T) {
|
||||
res, err := testClient.TableRows("books", RowsOptions{})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, 4, len(res.Columns))
|
||||
assert.Equal(t, 15, len(res.Rows))
|
||||
}
|
||||
|
||||
func test_TableInfo(t *testing.T) {
|
||||
res, err := testClient.TableInfo("books")
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, 4, len(res.Columns))
|
||||
assert.Equal(t, 1, len(res.Rows))
|
||||
}
|
||||
|
||||
func test_TableIndexes(t *testing.T) {
|
||||
res, err := testClient.TableIndexes("books")
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, 2, len(res.Columns))
|
||||
assert.Equal(t, 2, len(res.Rows))
|
||||
}
|
||||
|
||||
func test_Query(t *testing.T) {
|
||||
res, err := testClient.Query("SELECT * FROM books")
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, 4, len(res.Columns))
|
||||
assert.Equal(t, 15, len(res.Rows))
|
||||
}
|
||||
|
||||
func test_QueryError(t *testing.T) {
|
||||
res, err := testClient.Query("SELCT * FROM books")
|
||||
|
||||
assert.NotEqual(t, nil, err)
|
||||
assert.Equal(t, "pq: syntax error at or near \"SELCT\"", err.Error())
|
||||
assert.Equal(t, true, res == nil)
|
||||
}
|
||||
|
||||
func test_QueryInvalidTable(t *testing.T) {
|
||||
res, err := testClient.Query("SELECT * FROM books2")
|
||||
|
||||
assert.NotEqual(t, nil, err)
|
||||
assert.Equal(t, "pq: relation \"books2\" does not exist", err.Error())
|
||||
assert.Equal(t, true, res == nil)
|
||||
}
|
||||
|
||||
func test_ResultCsv(t *testing.T) {
|
||||
res, _ := testClient.Query("SELECT * FROM books ORDER BY id ASC LIMIT 1")
|
||||
csv := res.CSV()
|
||||
|
||||
expected := "id,title,author_id,subject_id\n156,The Tell-Tale Heart,115,9\n"
|
||||
|
||||
assert.Equal(t, expected, string(csv))
|
||||
}
|
||||
|
||||
func test_History(t *testing.T) {
|
||||
_, err := testClient.Query("SELECT * FROM books")
|
||||
query := testClient.history[len(testClient.history)-1].Query
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "SELECT * FROM books", query)
|
||||
}
|
||||
|
||||
func test_HistoryError(t *testing.T) {
|
||||
_, err := testClient.Query("SELECT * FROM books123")
|
||||
query := testClient.history[len(testClient.history)-1].Query
|
||||
|
||||
assert.NotEqual(t, nil, err)
|
||||
assert.NotEqual(t, "SELECT * FROM books123", query)
|
||||
}
|
||||
|
||||
func TestAll(t *testing.T) {
|
||||
if onWindows() {
|
||||
// Dont have access to windows machines at the moment...
|
||||
return
|
||||
}
|
||||
|
||||
setupCommands()
|
||||
teardown()
|
||||
setup()
|
||||
setupClient()
|
||||
|
||||
test_NewClientFromUrl(t)
|
||||
test_Test(t)
|
||||
test_Info(t)
|
||||
test_Databases(t)
|
||||
test_Tables(t)
|
||||
test_Table(t)
|
||||
test_TableRows(t)
|
||||
test_TableInfo(t)
|
||||
test_TableIndexes(t)
|
||||
test_Query(t)
|
||||
test_QueryError(t)
|
||||
test_QueryInvalidTable(t)
|
||||
test_ResultCsv(t)
|
||||
test_History(t)
|
||||
test_HistoryError(t)
|
||||
|
||||
teardownClient()
|
||||
teardown()
|
||||
}
|
||||
39
pkg/command/options.go
Normal file
39
pkg/command/options.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"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"`
|
||||
}
|
||||
|
||||
var Opts Options
|
||||
|
||||
func ParseOptions() error {
|
||||
_, err := flags.ParseArgs(&Opts, os.Args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if Opts.Url == "" {
|
||||
Opts.Url = os.Getenv("DATABASE_URL")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
100
pkg/connection/connection_string.go
Normal file
100
pkg/connection/connection_string.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"strings"
|
||||
|
||||
"github.com/sosedoff/pgweb/pkg/command"
|
||||
)
|
||||
|
||||
func currentUser() (string, error) {
|
||||
u, err := user.Current()
|
||||
if err == nil {
|
||||
return u.Username, nil
|
||||
}
|
||||
|
||||
name := os.Getenv("USER")
|
||||
if name != "" {
|
||||
return name, nil
|
||||
}
|
||||
|
||||
return "", errors.New("Unable to detect OS user")
|
||||
}
|
||||
|
||||
func FormatUrl(opts command.Options) (string, error) {
|
||||
url := opts.Url
|
||||
|
||||
// Make sure to only accept urls in a standard format
|
||||
if !strings.Contains(url, "postgres://") {
|
||||
return "", errors.New("Invalid URL. Valid format: postgres://user:password@host:port/db?sslmode=mode")
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
return url, nil
|
||||
}
|
||||
|
||||
func IsBlank(opts command.Options) bool {
|
||||
return opts.Host == "" && opts.User == "" && opts.DbName == "" && opts.Url == ""
|
||||
}
|
||||
|
||||
func BuildString(opts command.Options) (string, error) {
|
||||
if opts.Url != "" {
|
||||
return FormatUrl(opts)
|
||||
}
|
||||
|
||||
// Try to detect user from current OS user
|
||||
if opts.User == "" {
|
||||
u, err := currentUser()
|
||||
if err == nil {
|
||||
opts.User = u
|
||||
}
|
||||
}
|
||||
|
||||
// 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", opts.Pass)
|
||||
}
|
||||
|
||||
url += fmt.Sprintf("@%s:%d", opts.Host, opts.Port)
|
||||
|
||||
if opts.DbName != "" {
|
||||
url += fmt.Sprintf("/%s", opts.DbName)
|
||||
}
|
||||
|
||||
if opts.Ssl != "" {
|
||||
url += fmt.Sprintf("?sslmode=%s", opts.Ssl)
|
||||
}
|
||||
|
||||
return url, nil
|
||||
}
|
||||
165
pkg/connection/connection_string_test.go
Normal file
165
pkg/connection/connection_string_test.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/user"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Invalid_Url(t *testing.T) {
|
||||
opts := Options{}
|
||||
examples := []string{
|
||||
"postgresql://foobar",
|
||||
"foobar",
|
||||
}
|
||||
|
||||
for _, val := range examples {
|
||||
opts.Url = val
|
||||
str, err := buildConnectionString(opts)
|
||||
|
||||
assert.Equal(t, "", str)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Invalid URL. Valid format: postgres://user:password@host:port/db?sslmode=mode", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Valid_Url(t *testing.T) {
|
||||
url := "postgres://myhost/database"
|
||||
str, err := buildConnectionString(Options{Url: url})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, url, str)
|
||||
}
|
||||
|
||||
func Test_Url_And_Ssl_Flag(t *testing.T) {
|
||||
str, err := buildConnectionString(Options{
|
||||
Url: "postgres://myhost/database",
|
||||
Ssl: "disable",
|
||||
})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://myhost/database?sslmode=disable", str)
|
||||
}
|
||||
|
||||
func Test_Localhost_Url_And_No_Ssl_Flag(t *testing.T) {
|
||||
str, err := buildConnectionString(Options{
|
||||
Url: "postgres://localhost/database",
|
||||
})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://localhost/database?sslmode=disable", str)
|
||||
|
||||
str, err = buildConnectionString(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 := buildConnectionString(Options{
|
||||
Url: "postgres://localhost/database",
|
||||
Ssl: "require",
|
||||
})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://localhost/database?sslmode=require", str)
|
||||
|
||||
str, err = buildConnectionString(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 := buildConnectionString(Options{
|
||||
Url: "postgres://localhost/database?sslmode=require",
|
||||
})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://localhost/database?sslmode=require", str)
|
||||
|
||||
str, err = buildConnectionString(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 := buildConnectionString(Options{
|
||||
Host: "host",
|
||||
Port: 5432,
|
||||
User: "user",
|
||||
Pass: "password",
|
||||
DbName: "db",
|
||||
})
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://user:password@host:5432/db", str)
|
||||
}
|
||||
|
||||
func Test_Localhost(t *testing.T) {
|
||||
opts := Options{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
User: "user",
|
||||
Pass: "password",
|
||||
DbName: "db",
|
||||
}
|
||||
|
||||
str, err := buildConnectionString(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 = buildConnectionString(opts)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://user:password@127.0.0.1:5432/db?sslmode=disable", str)
|
||||
}
|
||||
|
||||
func Test_Localhost_And_Ssl(t *testing.T) {
|
||||
opts := Options{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
User: "user",
|
||||
Pass: "password",
|
||||
DbName: "db",
|
||||
Ssl: "require",
|
||||
}
|
||||
|
||||
str, err := buildConnectionString(opts)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://user:password@localhost:5432/db?sslmode=require", str)
|
||||
}
|
||||
|
||||
func Test_No_User(t *testing.T) {
|
||||
opts := Options{Host: "host", Port: 5432, DbName: "db"}
|
||||
u, _ := user.Current()
|
||||
str, err := buildConnectionString(opts)
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, fmt.Sprintf("postgres://%s@host:5432/db", u.Username), str)
|
||||
}
|
||||
|
||||
func Test_Port(t *testing.T) {
|
||||
opts := Options{Host: "host", User: "user", Port: 5000, DbName: "db"}
|
||||
str, err := buildConnectionString(opts)
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "postgres://user@host:5000/db", str)
|
||||
}
|
||||
|
||||
func Test_Blank(t *testing.T) {
|
||||
assert.Equal(t, true, connectionSettingsBlank(Options{}))
|
||||
assert.Equal(t, false, connectionSettingsBlank(Options{Host: "host", User: "user"}))
|
||||
assert.Equal(t, false, connectionSettingsBlank(Options{Host: "host", User: "user", DbName: "db"}))
|
||||
assert.Equal(t, false, connectionSettingsBlank(Options{Url: "url"}))
|
||||
}
|
||||
492
pkg/data/bindata.go
Normal file
492
pkg/data/bindata.go
Normal file
@@ -0,0 +1,492 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// bindata_read reads the given file from disk. It returns an error on failure.
|
||||
func bindata_read(path, name string) ([]byte, error) {
|
||||
buf, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error reading asset %s at %s: %v", name, path, err)
|
||||
}
|
||||
return buf, err
|
||||
}
|
||||
|
||||
type asset struct {
|
||||
bytes []byte
|
||||
info os.FileInfo
|
||||
}
|
||||
|
||||
// static_css_app_css reads file data from disk. It returns an error on failure.
|
||||
func static_css_app_css() (*asset, error) {
|
||||
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/css/app.css"
|
||||
name := "static/css/app.css"
|
||||
bytes, err := bindata_read(path, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
|
||||
}
|
||||
|
||||
a := &asset{bytes: bytes, info: fi}
|
||||
return a, err
|
||||
}
|
||||
|
||||
// static_css_bootstrap_css reads file data from disk. It returns an error on failure.
|
||||
func static_css_bootstrap_css() (*asset, error) {
|
||||
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/css/bootstrap.css"
|
||||
name := "static/css/bootstrap.css"
|
||||
bytes, err := bindata_read(path, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
|
||||
}
|
||||
|
||||
a := &asset{bytes: bytes, info: fi}
|
||||
return a, err
|
||||
}
|
||||
|
||||
// static_css_font_awesome_css reads file data from disk. It returns an error on failure.
|
||||
func static_css_font_awesome_css() (*asset, error) {
|
||||
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/css/font-awesome.css"
|
||||
name := "static/css/font-awesome.css"
|
||||
bytes, err := bindata_read(path, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
|
||||
}
|
||||
|
||||
a := &asset{bytes: bytes, info: fi}
|
||||
return a, err
|
||||
}
|
||||
|
||||
// static_fonts_fontawesome_otf reads file data from disk. It returns an error on failure.
|
||||
func static_fonts_fontawesome_otf() (*asset, error) {
|
||||
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/fonts/FontAwesome.otf"
|
||||
name := "static/fonts/FontAwesome.otf"
|
||||
bytes, err := bindata_read(path, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
|
||||
}
|
||||
|
||||
a := &asset{bytes: bytes, info: fi}
|
||||
return a, err
|
||||
}
|
||||
|
||||
// static_fonts_fontawesome_webfont_eot reads file data from disk. It returns an error on failure.
|
||||
func static_fonts_fontawesome_webfont_eot() (*asset, error) {
|
||||
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/fonts/fontawesome-webfont.eot"
|
||||
name := "static/fonts/fontawesome-webfont.eot"
|
||||
bytes, err := bindata_read(path, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
|
||||
}
|
||||
|
||||
a := &asset{bytes: bytes, info: fi}
|
||||
return a, err
|
||||
}
|
||||
|
||||
// static_fonts_fontawesome_webfont_svg reads file data from disk. It returns an error on failure.
|
||||
func static_fonts_fontawesome_webfont_svg() (*asset, error) {
|
||||
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/fonts/fontawesome-webfont.svg"
|
||||
name := "static/fonts/fontawesome-webfont.svg"
|
||||
bytes, err := bindata_read(path, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
|
||||
}
|
||||
|
||||
a := &asset{bytes: bytes, info: fi}
|
||||
return a, err
|
||||
}
|
||||
|
||||
// static_fonts_fontawesome_webfont_ttf reads file data from disk. It returns an error on failure.
|
||||
func static_fonts_fontawesome_webfont_ttf() (*asset, error) {
|
||||
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/fonts/fontawesome-webfont.ttf"
|
||||
name := "static/fonts/fontawesome-webfont.ttf"
|
||||
bytes, err := bindata_read(path, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
|
||||
}
|
||||
|
||||
a := &asset{bytes: bytes, info: fi}
|
||||
return a, err
|
||||
}
|
||||
|
||||
// static_fonts_fontawesome_webfont_woff reads file data from disk. It returns an error on failure.
|
||||
func static_fonts_fontawesome_webfont_woff() (*asset, error) {
|
||||
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/fonts/fontawesome-webfont.woff"
|
||||
name := "static/fonts/fontawesome-webfont.woff"
|
||||
bytes, err := bindata_read(path, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
|
||||
}
|
||||
|
||||
a := &asset{bytes: bytes, info: fi}
|
||||
return a, err
|
||||
}
|
||||
|
||||
// static_img_icon_ico reads file data from disk. It returns an error on failure.
|
||||
func static_img_icon_ico() (*asset, error) {
|
||||
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/img/icon.ico"
|
||||
name := "static/img/icon.ico"
|
||||
bytes, err := bindata_read(path, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
|
||||
}
|
||||
|
||||
a := &asset{bytes: bytes, info: fi}
|
||||
return a, err
|
||||
}
|
||||
|
||||
// static_img_icon_png reads file data from disk. It returns an error on failure.
|
||||
func static_img_icon_png() (*asset, error) {
|
||||
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/img/icon.png"
|
||||
name := "static/img/icon.png"
|
||||
bytes, err := bindata_read(path, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
|
||||
}
|
||||
|
||||
a := &asset{bytes: bytes, info: fi}
|
||||
return a, err
|
||||
}
|
||||
|
||||
// static_index_html reads file data from disk. It returns an error on failure.
|
||||
func static_index_html() (*asset, error) {
|
||||
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/index.html"
|
||||
name := "static/index.html"
|
||||
bytes, err := bindata_read(path, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
|
||||
}
|
||||
|
||||
a := &asset{bytes: bytes, info: fi}
|
||||
return a, err
|
||||
}
|
||||
|
||||
// static_js_ace_pgsql_js reads file data from disk. It returns an error on failure.
|
||||
func static_js_ace_pgsql_js() (*asset, error) {
|
||||
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/js/ace-pgsql.js"
|
||||
name := "static/js/ace-pgsql.js"
|
||||
bytes, err := bindata_read(path, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
|
||||
}
|
||||
|
||||
a := &asset{bytes: bytes, info: fi}
|
||||
return a, err
|
||||
}
|
||||
|
||||
// static_js_ace_js reads file data from disk. It returns an error on failure.
|
||||
func static_js_ace_js() (*asset, error) {
|
||||
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/js/ace.js"
|
||||
name := "static/js/ace.js"
|
||||
bytes, err := bindata_read(path, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
|
||||
}
|
||||
|
||||
a := &asset{bytes: bytes, info: fi}
|
||||
return a, err
|
||||
}
|
||||
|
||||
// static_js_app_js reads file data from disk. It returns an error on failure.
|
||||
func static_js_app_js() (*asset, error) {
|
||||
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/js/app.js"
|
||||
name := "static/js/app.js"
|
||||
bytes, err := bindata_read(path, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
|
||||
}
|
||||
|
||||
a := &asset{bytes: bytes, info: fi}
|
||||
return a, err
|
||||
}
|
||||
|
||||
// static_js_jquery_js reads file data from disk. It returns an error on failure.
|
||||
func static_js_jquery_js() (*asset, error) {
|
||||
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/js/jquery.js"
|
||||
name := "static/js/jquery.js"
|
||||
bytes, err := bindata_read(path, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
|
||||
}
|
||||
|
||||
a := &asset{bytes: bytes, info: fi}
|
||||
return a, err
|
||||
}
|
||||
|
||||
// Asset loads and returns the asset for the given name.
|
||||
// It returns an error if the asset could not be found or
|
||||
// could not be loaded.
|
||||
func Asset(name string) ([]byte, error) {
|
||||
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||
if f, ok := _bindata[cannonicalName]; ok {
|
||||
a, err := f()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
|
||||
}
|
||||
return a.bytes, nil
|
||||
}
|
||||
return nil, fmt.Errorf("Asset %s not found", name)
|
||||
}
|
||||
|
||||
// MustAsset is like Asset but panics when Asset would return an error.
|
||||
// It simplifies safe initialization of global variables.
|
||||
func MustAsset(name string) []byte {
|
||||
a, err := Asset(name)
|
||||
if (err != nil) {
|
||||
panic("asset: Asset(" + name + "): " + err.Error())
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// AssetInfo loads and returns the asset info for the given name.
|
||||
// It returns an error if the asset could not be found or
|
||||
// could not be loaded.
|
||||
func AssetInfo(name string) (os.FileInfo, error) {
|
||||
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||
if f, ok := _bindata[cannonicalName]; ok {
|
||||
a, err := f()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
|
||||
}
|
||||
return a.info, nil
|
||||
}
|
||||
return nil, fmt.Errorf("AssetInfo %s not found", name)
|
||||
}
|
||||
|
||||
// AssetNames returns the names of the assets.
|
||||
func AssetNames() []string {
|
||||
names := make([]string, 0, len(_bindata))
|
||||
for name := range _bindata {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// _bindata is a table, holding each asset generator, mapped to its name.
|
||||
var _bindata = map[string]func() (*asset, error){
|
||||
"static/css/app.css": static_css_app_css,
|
||||
"static/css/bootstrap.css": static_css_bootstrap_css,
|
||||
"static/css/font-awesome.css": static_css_font_awesome_css,
|
||||
"static/fonts/FontAwesome.otf": static_fonts_fontawesome_otf,
|
||||
"static/fonts/fontawesome-webfont.eot": static_fonts_fontawesome_webfont_eot,
|
||||
"static/fonts/fontawesome-webfont.svg": static_fonts_fontawesome_webfont_svg,
|
||||
"static/fonts/fontawesome-webfont.ttf": static_fonts_fontawesome_webfont_ttf,
|
||||
"static/fonts/fontawesome-webfont.woff": static_fonts_fontawesome_webfont_woff,
|
||||
"static/img/icon.ico": static_img_icon_ico,
|
||||
"static/img/icon.png": static_img_icon_png,
|
||||
"static/index.html": static_index_html,
|
||||
"static/js/ace-pgsql.js": static_js_ace_pgsql_js,
|
||||
"static/js/ace.js": static_js_ace_js,
|
||||
"static/js/app.js": static_js_app_js,
|
||||
"static/js/jquery.js": static_js_jquery_js,
|
||||
}
|
||||
|
||||
// AssetDir returns the file names below a certain
|
||||
// directory embedded in the file by go-bindata.
|
||||
// For example if you run go-bindata on data/... and data contains the
|
||||
// following hierarchy:
|
||||
// data/
|
||||
// foo.txt
|
||||
// img/
|
||||
// a.png
|
||||
// b.png
|
||||
// then AssetDir("data") would return []string{"foo.txt", "img"}
|
||||
// AssetDir("data/img") would return []string{"a.png", "b.png"}
|
||||
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
|
||||
// AssetDir("") will return []string{"data"}.
|
||||
func AssetDir(name string) ([]string, error) {
|
||||
node := _bintree
|
||||
if len(name) != 0 {
|
||||
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||
pathList := strings.Split(cannonicalName, "/")
|
||||
for _, p := range pathList {
|
||||
node = node.Children[p]
|
||||
if node == nil {
|
||||
return nil, fmt.Errorf("Asset %s not found", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if node.Func != nil {
|
||||
return nil, fmt.Errorf("Asset %s not found", name)
|
||||
}
|
||||
rv := make([]string, 0, len(node.Children))
|
||||
for name := range node.Children {
|
||||
rv = append(rv, name)
|
||||
}
|
||||
return rv, nil
|
||||
}
|
||||
|
||||
type _bintree_t struct {
|
||||
Func func() (*asset, error)
|
||||
Children map[string]*_bintree_t
|
||||
}
|
||||
var _bintree = &_bintree_t{nil, map[string]*_bintree_t{
|
||||
"static": &_bintree_t{nil, map[string]*_bintree_t{
|
||||
"css": &_bintree_t{nil, map[string]*_bintree_t{
|
||||
"app.css": &_bintree_t{static_css_app_css, map[string]*_bintree_t{
|
||||
}},
|
||||
"bootstrap.css": &_bintree_t{static_css_bootstrap_css, map[string]*_bintree_t{
|
||||
}},
|
||||
"font-awesome.css": &_bintree_t{static_css_font_awesome_css, map[string]*_bintree_t{
|
||||
}},
|
||||
}},
|
||||
"fonts": &_bintree_t{nil, map[string]*_bintree_t{
|
||||
"FontAwesome.otf": &_bintree_t{static_fonts_fontawesome_otf, map[string]*_bintree_t{
|
||||
}},
|
||||
"fontawesome-webfont.eot": &_bintree_t{static_fonts_fontawesome_webfont_eot, map[string]*_bintree_t{
|
||||
}},
|
||||
"fontawesome-webfont.svg": &_bintree_t{static_fonts_fontawesome_webfont_svg, map[string]*_bintree_t{
|
||||
}},
|
||||
"fontawesome-webfont.ttf": &_bintree_t{static_fonts_fontawesome_webfont_ttf, map[string]*_bintree_t{
|
||||
}},
|
||||
"fontawesome-webfont.woff": &_bintree_t{static_fonts_fontawesome_webfont_woff, map[string]*_bintree_t{
|
||||
}},
|
||||
}},
|
||||
"img": &_bintree_t{nil, map[string]*_bintree_t{
|
||||
"icon.ico": &_bintree_t{static_img_icon_ico, map[string]*_bintree_t{
|
||||
}},
|
||||
"icon.png": &_bintree_t{static_img_icon_png, map[string]*_bintree_t{
|
||||
}},
|
||||
}},
|
||||
"index.html": &_bintree_t{static_index_html, map[string]*_bintree_t{
|
||||
}},
|
||||
"js": &_bintree_t{nil, map[string]*_bintree_t{
|
||||
"ace-pgsql.js": &_bintree_t{static_js_ace_pgsql_js, map[string]*_bintree_t{
|
||||
}},
|
||||
"ace.js": &_bintree_t{static_js_ace_js, map[string]*_bintree_t{
|
||||
}},
|
||||
"app.js": &_bintree_t{static_js_app_js, map[string]*_bintree_t{
|
||||
}},
|
||||
"jquery.js": &_bintree_t{static_js_jquery_js, map[string]*_bintree_t{
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
}}
|
||||
|
||||
// Restore an asset under the given directory
|
||||
func RestoreAsset(dir, name string) error {
|
||||
data, err := Asset(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info, err := AssetInfo(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.MkdirAll(_filePath(dir, path.Dir(name)), os.FileMode(0755))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Restore assets under the given directory recursively
|
||||
func RestoreAssets(dir, name string) error {
|
||||
children, err := AssetDir(name)
|
||||
if err != nil { // File
|
||||
return RestoreAsset(dir, name)
|
||||
} else { // Dir
|
||||
for _, child := range children {
|
||||
err = RestoreAssets(dir, path.Join(name, child))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func _filePath(dir, name string) string {
|
||||
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||
return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
|
||||
}
|
||||
|
||||
21
pkg/history/history.go
Normal file
21
pkg/history/history.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package history
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Record struct {
|
||||
Query string `json:"query"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}
|
||||
|
||||
func New() []Record {
|
||||
return make([]Record, 0)
|
||||
}
|
||||
|
||||
func NewRecord(query string) Record {
|
||||
return Record{
|
||||
Query: query,
|
||||
Timestamp: time.Now().String(),
|
||||
}
|
||||
}
|
||||
47
pkg/statements/sql.go
Normal file
47
pkg/statements/sql.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package statements
|
||||
|
||||
const (
|
||||
PG_DATABASES = `SELECT datname FROM pg_database WHERE NOT datistemplate ORDER BY datname ASC`
|
||||
|
||||
PG_SCHEMAS = `SELECT schema_name FROM information_schema.schemata ORDER BY schema_name ASC`
|
||||
|
||||
PG_INFO = `SELECT
|
||||
session_user
|
||||
, current_user
|
||||
, current_database()
|
||||
, current_schemas(false)
|
||||
, inet_client_addr()
|
||||
, inet_client_port()
|
||||
, inet_server_addr()
|
||||
, inet_server_port()
|
||||
, version()`
|
||||
|
||||
PG_TABLE_INDEXES = `SELECT indexname, indexdef FROM pg_indexes WHERE tablename = $1`
|
||||
|
||||
PG_TABLE_INFO = `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`
|
||||
|
||||
PG_TABLE_SCHEMA = `SELECT
|
||||
column_name, data_type, is_nullable, character_maximum_length, character_set_catalog, column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = $1`
|
||||
|
||||
PG_TABLES = `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_schema,table_name`
|
||||
|
||||
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`
|
||||
)
|
||||
32
pkg/util/profiler.go
Normal file
32
pkg/util/profiler.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
const MEGABYTE = 1024 * 1024
|
||||
|
||||
func runProfiler() {
|
||||
logger := log.New(os.Stdout, "", 0)
|
||||
m := &runtime.MemStats{}
|
||||
|
||||
for {
|
||||
runtime.ReadMemStats(m)
|
||||
|
||||
logger.Printf(
|
||||
"[DEBUG] Goroutines: %v, Mem used: %v (%v mb), Mem acquired: %v (%v mb)\n",
|
||||
runtime.NumGoroutine(),
|
||||
m.Alloc, m.Alloc/MEGABYTE,
|
||||
m.Sys, m.Sys/MEGABYTE,
|
||||
)
|
||||
|
||||
time.Sleep(time.Second * 30)
|
||||
}
|
||||
}
|
||||
|
||||
func StartProfiler() {
|
||||
go runProfiler()
|
||||
}
|
||||
Reference in New Issue
Block a user