Compare commits

...

113 Commits
v0.1 ... master

Author SHA1 Message Date
kolaente ddc3477efd
Make it work with modules 2021-05-16 00:39:09 +02:00
konrad 0b1212f428
fmt
the build was successful Details
2018-04-13 15:05:23 +02:00
konrad c097264bde
Made tests work again
the build failed Details
2018-04-13 15:03:36 +02:00
konrad 2bc349a33b
Added test fixtures 2018-04-13 15:03:22 +02:00
konrad 5673acdd60
Fixed lint 2018-04-13 14:41:31 +02:00
konrad 0b4891c3ef
Added translations for viewing logs
the build failed Details
2018-03-07 16:19:56 +01:00
kolaente db5e3a7cbc
When viewing logs via the ui, you can now click on the item to show it 2018-03-07 16:07:56 +01:00
konrad 0030372879
started adding view logs via ui
the build failed Details
2018-03-06 16:32:28 +01:00
konrad 3de6ecf579
Added viewing logs via api 2018-03-06 15:33:18 +01:00
konrad 91e10eb78b
Fixed doer missing
the build failed Details
2018-03-06 14:01:46 +01:00
konrad 5fbc08cf65
fmt
the build failed Details
2018-03-06 13:43:07 +01:00
konrad 0763ae07b0
Properly implemented logging
the build failed Details
2018-03-06 12:36:49 +01:00
konrad d553ca743b
Moved to new namespace
the build was successful Details
2018-03-05 12:53:12 +01:00
konrad 8565772ab0
Added package-log.json 2018-03-05 12:49:26 +01:00
konrad cfb68b9f79
Updated makefile to work with go 1.10
the build was successful Details
2018-03-05 12:12:20 +01:00
konrad 7a3bc05243
Fixed typo in readme
the build was successful Details
2018-03-05 12:10:10 +01:00
kolaente 5b7fb1be87
Merge remote-tracking branch 'origin/master'
the build failed Details
2018-03-05 11:09:59 +01:00
konrad 0bf37c2ac0
Fixed build 2018-03-05 11:09:41 +01:00
konrad eebb8eb4c4 'Readme.md' ändern
the build failed Details
2018-03-04 22:51:43 +00:00
konrad d46cbc2d67
fmt
the build failed Details
2018-01-30 12:44:22 +01:00
konrad 537aebc873
Added tests for deleting the last user
the build failed Details
2018-01-30 12:40:47 +01:00
konrad 152a399567
Don't delete a user if its the last one
the build failed Details
2018-01-30 12:22:00 +01:00
kolaente ca88232109
Fixed cors 2018-01-30 11:23:00 +01:00
kolaente 12d8b8f0fa
Added frontend links to manage users + view logs
the build failed Details
2018-01-29 16:38:59 +01:00
kolaente 56a3215147
Return 400 instead of 500 when deleting or showing something where the id is not an int
the build failed Details
2018-01-29 16:04:20 +01:00
kolaente 989ab530dd
Return 400 instead of 500 when updateing something where the id is not an int 2018-01-29 15:46:56 +01:00
kolaente 80f86527ed
Obfuscated user passwords when returning a user object 2018-01-29 15:33:38 +01:00
kolaente 87529b5e3e
Updated Readme
the build was successful Details
2018-01-27 16:47:12 +01:00
kolaente b6d0570e79
Updated gitignore
the build was successful Details
2018-01-27 14:00:23 +01:00
konrad e99aeb67c6
fmt
the build was successful Details
2018-01-26 16:44:58 +01:00
konrad f09e5c6f84
Added check if the emailaddress is already used when creating a new user 2018-01-26 16:00:18 +01:00
konrad 63a74ee7ac
Fixed a bug where a user couldn't update its own password
the build failed Details
2018-01-26 15:09:33 +01:00
konrad aa5b510424
Improved unit tests
the build failed Details
2018-01-26 12:37:11 +01:00
konrad 7ddbec7a6f
Added more unit tests for authors 2018-01-26 12:29:16 +01:00
kolaente ab19edfbd3
updated gitignore
the build was successful Details
2018-01-25 22:31:50 +01:00
konrad 3b38e61ab8
Updated gitignore
the build was successful Details
2018-01-25 16:33:34 +01:00
konrad 96ad0fc31b
Small improvements to unit tests
the build was successful Details
2018-01-25 16:28:02 +01:00
konrad 66b6dbf4f3
Small improvements to unit tests 2018-01-25 14:39:27 +01:00
konrad 70d6278540
removed unused lib
the build was successful Details
2018-01-25 12:27:48 +01:00
konrad 068bfc942e
Beautified error messages
the build failed Details
2018-01-25 12:23:51 +01:00
kolaente a734f21ac2
Added custom error type when no username and password are given when creating a new user 2018-01-25 12:15:12 +01:00
konrad 3d27bb1438
Added custom error type when a publisher has no name
the build failed Details
2018-01-24 23:55:10 +01:00
konrad 815ec40696
removed unused lib 2018-01-24 13:20:46 +01:00
konrad a4b8a44e47
Added custom error for could not get User ID from JWT 2018-01-24 13:18:17 +01:00
konrad c8da860eab
Modified http status code on error 2018-01-24 13:15:28 +01:00
konrad dec5db7649
Added custom error type for no book title 2018-01-24 13:13:55 +01:00
konrad c3cfc73840
Added error type for no item title 2018-01-24 13:04:47 +01:00
konrad 2e2877156b
Added custom error type for ID cannot be 0 2018-01-24 12:58:00 +01:00
konrad d309d5b3b6
fixed lint + gofmt
the build was successful Details
2018-01-23 16:20:37 +01:00
konrad bcb8b08001
Added check if the wants to change its own password
the build failed Details
2018-01-23 15:58:01 +01:00
konrad 434856a44f
Added route to update a user's password
the build failed Details
2018-01-23 15:53:38 +01:00
konrad a92d711b00
Added admincheck to all adminroutes 2018-01-23 15:27:08 +01:00
konrad 4275a3acad
Added method to delete a user 2018-01-23 15:25:41 +01:00
konrad 0027b1f001
Added method to get information about a user
the build failed Details
2018-01-23 15:20:02 +01:00
konrad 323f4c7ff4
Added custom error type for user does not exist 2018-01-23 15:19:39 +01:00
konrad aaf5ca0ceb
Added custom error type for no username provided 2018-01-23 14:52:03 +01:00
konrad f7314b439f
gofmt
the build was successful Details
2018-01-23 14:32:23 +01:00
konrad f27172cfd8
Added method to add or update a user 2018-01-23 14:31:54 +01:00
konrad d7fd1082a4
Fixed lint 2018-01-23 13:00:32 +01:00
konrad 4b82af8ae8
Added method to update a user 2018-01-23 12:59:48 +01:00
konrad 772ed316cb
Added list user method 2018-01-23 12:37:13 +01:00
konrad dbc3886706
Added admin type user 2018-01-23 11:20:22 +01:00
konrad 6c1ecf55f9
Updated Systemd Template
the build was successful Details
2018-01-22 16:05:54 +01:00
konrad 030e2af33d
Added systemd template
the build was successful Details
2018-01-16 18:12:33 +01:00
konrad 24d2a6519f
Updated version
the build was successful Details
2018-01-16 17:17:59 +01:00
konrad a01f0a7b33
gofmt
the build was successful Details
2018-01-16 16:24:29 +01:00
konrad 1b6a304ae8
Added unit tests for user 2018-01-16 16:24:21 +01:00
konrad 3c5af74141
Added unit tests for status 2018-01-16 16:13:17 +01:00
konrad b613208950
Added unit tests for quantity itself 2018-01-16 16:07:21 +01:00
konrad 3822e4a09e
Added unit test for item quantities
the build failed Details
2018-01-16 16:01:42 +01:00
konrad 8da6ca8e0b
Added todo 2018-01-16 15:59:16 +01:00
konrad 7dd2523a00
Fixed getting quantity 2018-01-16 15:59:05 +01:00
konrad a809cbd485
Added Unit tests for quantity 2018-01-16 15:58:16 +01:00
konrad f4b8d07d0e
Added Unit tests for creating xorm engine 2018-01-16 15:05:39 +01:00
konrad 2df5cd1694
Added unit tests for config 2018-01-16 15:05:22 +01:00
konrad d171d1f787
Book unit test improvements 2018-01-16 14:55:45 +01:00
konrad df0482dac2
Gofmt 2018-01-16 14:28:44 +01:00
konrad 9c7d2ca000
Added unit tests for publishers 2018-01-16 14:28:22 +01:00
konrad 7e40119014
Fixed error when inserting an empty publisher 2018-01-16 14:28:02 +01:00
konrad 310d15d078
Improved book unit tests 2018-01-16 14:21:02 +01:00
konrad 97ebfc0207
Fixed error when inserting an empty item 2018-01-16 14:11:46 +01:00
konrad 547b44d608
goft 2018-01-16 14:06:09 +01:00
konrad a315f71144
Improved author unit test 2018-01-16 13:19:49 +01:00
konrad bd261ca375
Improved books unit test 2018-01-16 13:14:56 +01:00
konrad 64ae3244ed
Improved author unit test 2018-01-16 13:12:37 +01:00
konrad ac78d311a8
typo 2018-01-16 12:36:50 +01:00
konrad f00f473df5
Added unit tests for books 2018-01-16 12:34:08 +01:00
konrad 14651d198c
Improved Author unit tests 2018-01-16 12:27:14 +01:00
konrad adf7700a8a
Added libs
the build was successful Details
2018-01-16 11:56:31 +01:00
konrad ed38925489
Fixed lint + gofmt
the build failed Details
2018-01-16 11:55:12 +01:00
konrad d0202aea41
Added author tests 2018-01-16 11:53:27 +01:00
konrad aa6bc74405
Started adding unit tests
the build failed Details
2018-01-16 11:41:11 +01:00
konrad 6868833854
Improved Loading spinner 2018-01-16 10:38:36 +01:00
konrad 01d3c27d82
Updated version
the build was successful Details
2018-01-16 09:58:34 +01:00
konrad 3f147ce7c5
Fixed drone config to also build and release frontend when tagging 2018-01-16 09:58:09 +01:00
konrad 224e795531
Improved error messages
the build failed Details
2018-01-15 13:20:58 +01:00
konrad d2476e061f
comment 2018-01-15 13:12:26 +01:00
konrad 1911ec4d5f
The api now returns more specific error codes
the build failed Details
2018-01-15 13:08:17 +01:00
konrad 82d596994a
Fixed commas not created when more than two authors 2018-01-15 12:20:24 +01:00
konrad 3996f32e04
Fixed sort icons not showing in grid overview 2018-01-15 12:17:36 +01:00
konrad aaee574f8f
Fixed Releasing os packages
the build was successful Details
2018-01-12 11:20:59 +01:00
konrad 50980a0fce
Added download badge to readme
the build failed Details
2018-01-12 10:42:31 +01:00
konrad 782135e105
Modified makefile to copy license when building release 2018-01-12 10:33:48 +01:00
konrad baa53b8972
Added License 2018-01-12 10:31:40 +01:00
konrad ff6a1f6d59
Modified build process to build the frontend only once
the build failed Details
2018-01-12 10:17:21 +01:00
konrad 48c9ab08c9
Modified build process to build the frontend only once
the build failed Details
2018-01-12 10:14:08 +01:00
konrad aa1010d74a
Modified build process to build the frontend only once
the build failed Details
2018-01-12 10:11:56 +01:00
konrad b2e086c3e6
Added release download link to readme 2018-01-12 10:04:42 +01:00
konrad ea3b6fcc43
Fixed makefile to not create packages for .sha256 files
the build failed Details
2018-01-11 16:26:55 +01:00
konrad 9527d086b2
Fixed frontend static build
the build failed Details
2018-01-11 15:19:45 +01:00
konrad ce73d46a8c
Modified drone config to parallise static builds 2018-01-11 15:15:43 +01:00
konrad 7e7cb0d699
Modified drone config to parallise static builds
the build failed Details
2018-01-11 15:15:22 +01:00
konrad 02c855ee52
Modified drone config to parallise static builds
the build failed Details
2018-01-11 15:13:48 +01:00
468 changed files with 117918 additions and 22705 deletions

View File

@ -1,6 +1,6 @@
workspace:
base: /srv/app
path: src/git.mowie.cc/konrad/Library
path: src/git.kolaente.de/konrad/Library
clone:
git:
@ -25,29 +25,100 @@ pipeline:
event: [ push, tag, pull_request ]
build-frontend:
image: webhippie/nodejs:current
image: node:latest
pull: true
group: build
commands:
- cd frontend/
- rm siteconfig.json
- "echo '{\"API_URL\": \"/api/v1/\"}' > siteconfig.json"
- npm install
- npm run build
when:
event: [ push, tag, pull_request ]
test-backend:
image: webhippie/golang:edge
pull: true
environment:
GOPATH: /srv/app
commands:
- make test
when:
event: [ push, tag, pull_request ]
# Build a release when tagging
static:
before-static-build:
image: karalabe/xgo-latest:latest
pull: true
environment:
TAGS: bindata sqlite
GOPATH: /srv/app
commands:
- make release
- make release-dirs
when:
event: [tag, push]
static-build-windows:
image: karalabe/xgo-latest:latest
pull: true
group: build-static
environment:
TAGS: bindata sqlite
GOPATH: /srv/app
commands:
- make release-windows
when:
event: [tag, push]
static-build-linux:
image: karalabe/xgo-latest:latest
pull: true
group: build-static
environment:
TAGS: bindata sqlite
GOPATH: /srv/app
commands:
- make release-linux
when:
event: [tag, push]
static-build-darwin:
image: karalabe/xgo-latest:latest
pull: true
group: build-static
environment:
TAGS: bindata sqlite
GOPATH: /srv/app
commands:
- make release-darwin
when:
event: [tag, push]
# static-build-frontend:
# image: webhippie/nodejs:current
# pull: true
# group: build-static
# commands:
# - make release-frontend
# when:
# event: [push, tag ]
after-build-static:
image: karalabe/xgo-latest:latest
pull: true
environment:
TAGS: bindata sqlite
GOPATH: /srv/app
commands:
- make release-copy
- make release-check
- make release-os-package
- make release-zip
when:
event: [tag, push]
# Push the releases to our pseudo-s3-bucket
release:
image: plugins/s3:1
pull: true

12
.gitignore vendored
View File

@ -1,11 +1,17 @@
.idea/*
# Go stuff
Library
dist/*
dist/
config.ini
*.db
cover.html
coverage.out
# Frontend stuff
frontend/node_modules/
frontend/dist/
frontend/npm-debug.log*
frontend/yarn-debug.log*
frontend/yarn-error.log*
config.ini
db.db
frontend/yarn-error.log*

165
LICENSE Normal file
View File

@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

View File

@ -1,5 +1,5 @@
DIST := dist
IMPORT := git.mowie.cc/konrad/Library
IMPORT := git.kolaente.de/konrad/Library
SED_INPLACE := sed -i
@ -21,7 +21,7 @@ EXTRA_GOFLAGS ?=
LDFLAGS := -X "main.Version=$(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')" -X "main.Tags=$(TAGS)"
PACKAGES ?= $(filter-out git.mowie.cc/konrad/Library/integrations,$(shell go list ./... | grep -v /vendor/))
PACKAGES ?= $(filter-out git.kolaente.de/konrad/Library/integrations,$(shell go list ./... | grep -v /vendor/))
SOURCES ?= $(shell find . -name "*.go" -type f)
TAGS ?=
@ -52,8 +52,12 @@ clean:
go clean -i ./...
rm -rf $(EXECUTABLE) $(DIST) $(BINDATA)
.PHONY: test
test:
go test -cover $(PACKAGES)
required-gofmt-version:
@go version | grep -q '\(1.7\|1.8\|1.9\)' || { echo "We require go version 1.7 or 1.8 or 1.9 to format code" >&2 && exit 1; }
@go version | grep -q '\(1.7\|1.8\|1.9\|1.10\)' || { echo "We require go version 1.7, 1.8, 1.9 or 1.10 to format code" >&2 && exit 1; }
.PHONY: lint
lint:
@ -136,7 +140,6 @@ release-copy:
$(foreach file,$(wildcard $(DIST)/binaries/$(EXECUTABLE)-*),cp $(file) $(DIST)/release/$(notdir $(file));)
mkdir $(DIST)/release/frontend
cp frontend/dist $(DIST)/release/frontend/ -R
cp config.ini.sample $(DIST)/release/config.ini
.PHONY: release-check
release-check:
@ -145,8 +148,8 @@ release-check:
.PHONY: release-os-package
release-os-package:
$(foreach file,$(wildcard $(DIST)/release/$(EXECUTABLE)-*),mkdir $(file)-full;mv $(file) $(file)-full/; mv $(file).sha256 $(file)-full/; cp config.ini.sample $(file)-full/config.ini; cp $(DIST)/release/frontend $(file)-full/ -R; )
rm $(DIST)/release/frontend $(DIST)/release/config.ini -rf
$(foreach file,$(filter-out %.sha256,$(wildcard $(DIST)/release/$(EXECUTABLE)-*)),mkdir $(file)-full;mv $(file) $(file)-full/; mv $(file).sha256 $(file)-full/; cp config.ini.sample $(file)-full/config.ini; cp $(DIST)/release/frontend $(file)-full/ -R; cp LICENSE $(file)-full/; )
rm $(DIST)/release/frontend -rf
.PHONY: release-zip
release-zip:

View File

@ -1,7 +1,12 @@
# Library
[![Build Status](https://drone.mowie.cc/api/badges/konrad/Library/status.svg)](https://drone.mowie.cc/konrad/Library)
[![Build Status](https://drone.kolaente.de/api/badges/konrad/Library/status.svg)](https://drone.kolaente.de/konrad/Library)
[![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](LICENSE)
[![Download](https://img.shields.io/badge/download-v0.2-brightgreen.svg)](https://storage.kolaente.de/minio/library-release/)
An application to manage your library, books and authors.
API Docs: https://git.mowie.cc/konrad/Library/wiki/API
API Docs: https://git.kolaente.de/konrad/Library/wiki/API
Download the latest release: https://storage.kolaente.de/minio/library-release/
(`master` is up-to-date with the master branch and can contain bugs, if you want a stable version, choose a version from the list)

11638
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1 @@
{
"API_URL": "http://localhost:8082/api/v1/"
}
{"API_URL": "/api/v1/"}

View File

@ -1,23 +1,29 @@
<template>
<div id="app">
<template v-if="user.authenticated">
<div class="ui secondary menu">
<router-link to="/" class="item" v-lang.nav.home></router-link>
<router-link to="/books" class="item" v-lang.nav.books></router-link>
<router-link to="/authors" class="item" v-lang.nav.authors></router-link>
<router-link to="/publishers" class="item" v-lang.nav.publishers></router-link>
<router-link to="/items" class="item" v-lang.nav.items></router-link>
<div class="right menu">
<img v-bind:src="gravatar" class="menu-avatar"/>
<div class="ui item"> {{ user.infos.username }}</div>
<a class="ui item" @click="logout()">
<icon name="sign-out"></icon>
</a>
<div class="menu-wrapper">
<div class="ui secondary menu">
<router-link to="/" class="item" v-lang.nav.home></router-link>
<router-link to="/books" class="item" v-lang.nav.books></router-link>
<router-link to="/authors" class="item" v-lang.nav.authors></router-link>
<router-link to="/publishers" class="item" v-lang.nav.publishers></router-link>
<router-link to="/items" class="item" v-lang.nav.items></router-link>
<router-link to="/users" class="item" v-if="user.infos.admin" v-lang.nav.users></router-link>
<router-link to="/logs" class="item" v-if="user.infos.admin" v-lang.nav.logs></router-link>
<div class="right menu">
<img v-bind:src="gravatar" class="menu-avatar"/>
<div class="ui item"> {{ user.infos.username }}</div>
<a class="ui item" @click="logout()">
<icon name="sign-out"></icon>
</a>
</div>
</div>
<div class="ui divider"></div>
</div>
<div class="ui divider"></div>
</template>
<router-view/>
<div class="content">
<router-view/>
</div>
<div class="lang-switcher">
<a @click="language = 'en'"><i class="gb flag"></i></a>
@ -47,6 +53,8 @@ export default {
router.push({ name: 'login' })
}
console.log(this.user.infos)
// Set the users avatar
this.setAvatar()
},
@ -107,4 +115,69 @@ export default {
transition: color .1s ease;
font-weight: 400;
}
.menu-wrapper {
position: absolute;
z-index: 6;
left: 0;
right: 0;
top: 1em;
}
.content {
padding-top: 70px;
}
/* spinner */
.fullscreen-loader-wrapper {
position: fixed;
background: rgba(250,250,250,0.8);
z-index: 5;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.half-circle-spinner, .half-circle-spinner * {
box-sizing: border-box;
}
.half-circle-spinner {
width: 60px;
height: 60px;
border-radius: 100%;
position: relative;
left: calc(50% - 30px);
top: calc(50% - 30px);
}
.half-circle-spinner .circle {
content: "";
position: absolute;
width: 100%;
height: 100%;
border-radius: 100%;
border: calc(60px / 10) solid transparent;
}
.half-circle-spinner .circle.circle-1 {
border-top-color: #4CAF50;
animation: half-circle-spinner-animation 1s infinite;
}
.half-circle-spinner .circle.circle-2 {
border-bottom-color: #4CAF50;
animation: half-circle-spinner-animation 1s infinite alternate;
}
@keyframes half-circle-spinner-animation {
0% {
transform: rotate(0deg);
}
100%{
transform: rotate(360deg);
}
}
</style>

View File

@ -2,9 +2,11 @@
<div v-if="user.authenticated">
<h1>{{ author.forename }} {{ author.lastname }}</h1>
<div class="ui info message" v-if="loading">
<icon name="refresh" spin></icon>&nbsp;&nbsp;
<span v-lang.general.loading></span>
<div class="fullscreen-loader-wrapper" v-if="loading">
<div class="half-circle-spinner">
<div class="circle circle-1"></div>
<div class="circle circle-2"></div>
</div>
</div>
<div v-if="!loading">

View File

@ -2,9 +2,11 @@
<div v-if="user.authenticated">
<h1 v-lang.authors.title></h1>
<div class="ui info message" v-if="loading">
<icon name="refresh" spin></icon>&nbsp;&nbsp;
<span v-lang.general.loading></span>
<div class="fullscreen-loader-wrapper" v-if="loading">
<div class="half-circle-spinner">
<div class="circle circle-1"></div>
<div class="circle circle-2"></div>
</div>
</div>
<div v-if="!loading">
@ -181,6 +183,7 @@ export default {
DeleteAuthor (obj) {
this.showModal = true
this.$on('delete-submit', function () {
this.loading = true
// Prevent deleting already deleted authors
if (obj) {
HTTP.delete('authors/' + obj.id.content, { headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')} })

View File

@ -2,9 +2,11 @@
<div v-if="user.authenticated">
<h1>{{ book.title }}</h1>
<div class="ui info message" v-if="loading">
<icon name="refresh" spin></icon>&nbsp;&nbsp;
<span v-lang.general.loading></span>
<div class="fullscreen-loader-wrapper" v-if="loading">
<div class="half-circle-spinner">
<div class="circle circle-1"></div>
<div class="circle circle-2"></div>
</div>
</div>
<div v-if="!loading">

View File

@ -2,9 +2,11 @@
<div v-if="user.authenticated">
<h1 v-lang.books.title></h1>
<div class="ui info message" v-if="loading">
<icon name="refresh" spin></icon>&nbsp;&nbsp;
<span v-lang.general.loading></span>
<div class="fullscreen-loader-wrapper" v-if="loading">
<div class="half-circle-spinner">
<div class="circle circle-1"></div>
<div class="circle circle-2"></div>
</div>
</div>
<div v-if="!loading">
@ -187,7 +189,7 @@ export default {
let authors = bs[b].authors
for (const au in authors) {
this.books[i].author += authors[au].forename + ' ' + authors[au].lastname
if (authors.length > au + 1) {
if ((authors.length - 1) > au) {
this.books[i].author += ', '
}
}
@ -235,6 +237,7 @@ export default {
deleteBook (obj) {
this.showModal = true
this.$on('delete-submit', function () {
this.loading = true
// Prevent deleting already deleted books
if (obj) {
HTTP.delete('books/' + obj.id.content, { headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')} })

View File

@ -2,9 +2,11 @@
<div v-if="user.authenticated">
<h1>{{ item.title }}</h1>
<div class="ui info message" v-if="loading">
<icon name="refresh" spin></icon>&nbsp;&nbsp;
<span v-lang.general.loading></span>
<div class="fullscreen-loader-wrapper" v-if="loading">
<div class="half-circle-spinner">
<div class="circle circle-1"></div>
<div class="circle circle-2"></div>
</div>
</div>
<div v-if="!loading">

View File

@ -2,9 +2,11 @@
<div v-if="user.authenticated">
<h1 v-lang.items.title></h1>
<div class="ui info message" v-if="loading">
<icon name="refresh" spin></icon>&nbsp;&nbsp;
<span v-lang.general.loading></span>
<div class="fullscreen-loader-wrapper" v-if="loading">
<div class="half-circle-spinner">
<div class="circle circle-1"></div>
<div class="circle circle-2"></div>
</div>
</div>
<div v-if="!loading">
@ -188,6 +190,7 @@ export default {
DeleteItem (obj) {
this.showModal = true
this.$on('delete-submit', function () {
this.loading = true
// Prevent deleting already deleted item
if (obj) {
HTTP.delete('items/' + obj.id.content, { headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')} })

View File

@ -0,0 +1,227 @@
<template>
<div v-if="user.authenticated">
<h1 v-lang.logs.title></h1>
<div class="fullscreen-loader-wrapper" v-if="loading">
<div class="half-circle-spinner">
<div class="circle circle-1"></div>
<div class="circle circle-2"></div>
</div>
</div>
<div v-if="!loading">
<button @click="loadLogs()" class="ui teal labeled icon button" style="float: right;">
<i class="refresh icon"></i>
<span v-lang.general.refresh></span>
</button>
<form id="search">
<div class="ui icon input">
<input :placeholder="langGeneral.search" type="text" v-model="searchQuery" v-focus>
<i class="search icon"></i>
</div>
</form>
<paginate
name="logs"
:list="filteredData"
:per="35"
tag="div"
>
<grid
:data="paginated('logs')"
:columns="gridColumns"
>
</grid>
</paginate>
<div class="pagination-container">
<paginate-links
tag="div"
for="logs"
:hide-single-page="true"
:classes="{
'ul': ['ui', 'pagination', 'menu'],
'li': 'item',
'li a': 'pagination-link'
}"
>
</paginate-links>
<div v-if="$refs.paginator" v-lang.general.searchResultCount="$refs.paginator.pageItemsCount"></div>
</div>
</div>
</div>
</template>
<script>
import auth from '../auth'
import {HTTP} from '../http-common'
export default {
name: 'Logs',
data () {
return {
user: auth.user,
logs: [],
searchQuery: '',
gridColumns: [],
loading: false,
paginate: ['logs'],
allStatus: [],
showModal: false,
logActions: []
}
},
created () {
this.loadLogs()
// Grid
this.gridColumns = [
this.translate('logs').gridColumns.user,
this.translate('logs').gridColumns.action,
this.translate('logs').gridColumns.itemID,
this.translate('logs').gridColumns.date
]
// Build the list with all logactions
this.logActions = this.translate('logs').logActions
// Set the title
document.title = this.translate('nav').logs
},
watch: {
// call again the method if the route changes
'$route': 'loadLogs'
},
computed: {
filteredData: function () {
var filterKey = this.searchQuery && this.searchQuery.toLowerCase()
var data = this.logs
if (filterKey) {
data = data.filter(function (row) {
return Object.keys(row).some(function (key) {
if (row[key].content) {
return String(row[key].content).toLowerCase().indexOf(filterKey) > -1
} else {
return String(row[key]).toLowerCase().indexOf(filterKey) > -1
}
})
})
}
return data
},
langGeneral () {
return this.translate('general')
}
},
methods: {
errorNotification (e) {
// Build the notification text from error response
let err = e.message
if (e.response.data && e.response.data.message) {
err += '<br/>' + e.response.data.message
}
// Fire a notification
this.$notify({
type: 'error',
title: this.langGeneral.error,
text: err
})
},
loadLogs () {
this.loading = true
this.logs = []
HTTP.get(`logs`, { headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')} })
.then(response => {
let ls = response.data
let i = 0
// Loop throught the data we got from our API and prepare an array to display all authors
for (const l in ls) {
// Beautify the date
let c = new Date(ls[l].time * 1000)
let time = {
date: ('0' + c.getDate()).slice(-2) + '.' + ('0' + (c.getMonth() + 1)).slice(-2) + '.' + c.getFullYear(),
time: ('0' + c.getHours()).slice(-2) + ':' + ('0' + c.getMinutes()).slice(-2)
}
let logAction = ls[l].log
this.logs[i] = {
id: {content: ls[l].id, hide: true}, // Don't show the id
user: ls[l].userID,
log: this.logActions[logAction],
item: {content: ls[l].itemID, link: '#'},
time: time.date + ' ' + time.time
}
// Build the item link
// Book
if (logAction === 1 || logAction === 2 || logAction === 3) {
this.logs[i].item.link = '/books/' + ls[l].itemID
}
// Author
if (logAction === 4 || logAction === 5 || logAction === 6) {
this.logs[i].item.link = '/authors/' + ls[l].itemID
}
// Publisher
if (logAction === 7 || logAction === 8 || logAction === 9) {
this.logs[i].item.link = '/publishers/' + ls[l].itemID
}
// Item
if (logAction === 10 || logAction === 11 || logAction === 12) {
this.logs[i].item.link = '/items/' + ls[l].itemID
}
// User
if (logAction === 13 || logAction === 14 || logAction === 15 || logAction === 16) {
this.logs[i].item.link = '/users/' + ls[l].itemID
}
// increment dat shit
i++
}
this.loading = false
})
.catch(e => {
this.loading = false
this.errorNotification(e)
})
}
}
}
</script>
<style>
a.pagination-link{
margin: -5px -1.14286em -18px;
display: block;
position: absolute;
cursor: pointer;
padding: 0.928571em 1.14286em;
color: rgba(0,0,0,.87);
-webkit-transition: background-color 200ms; /* Safari */
transition: background-color 200ms;
}
a.pagination-link:hover{
background: rgba(0,0,0,.02);
}
.pagination{
padding: 0;
}
.pagination-container{
margin-top: 1rem;
text-align: center;
}
#search{
margin-bottom: 1rem;
}
</style>

View File

@ -2,9 +2,11 @@
<div v-if="user.authenticated">
<h1>{{ publisher.name }}</h1>
<div class="ui info message" v-if="loading">
<icon name="refresh" spin></icon>&nbsp;&nbsp;
<span v-lang.general.loading></span>
<div class="fullscreen-loader-wrapper" v-if="loading">
<div class="half-circle-spinner">
<div class="circle circle-1"></div>
<div class="circle circle-2"></div>
</div>
</div>
<div v-if="!loading">

View File

@ -2,9 +2,11 @@
<div v-if="user.authenticated">
<h1 v-lang.publishers.title></h1>
<div class="ui info message" v-if="loading">
<icon name="refresh" spin></icon>&nbsp;&nbsp;
<span v-lang.general.loading></span>
<div class="fullscreen-loader-wrapper" v-if="loading">
<div class="half-circle-spinner">
<div class="circle circle-1"></div>
<div class="circle circle-2"></div>
</div>
</div>
<div v-if="!loading">
@ -180,6 +182,7 @@ export default {
DeletePublisher (obj) {
this.showModal = true
this.$on('delete-submit', function () {
this.loading = true
// Prevent again deleting already deleted publishers
if (obj) {
HTTP.delete('publishers/' + obj.id.content, { headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')} })

View File

@ -36,7 +36,9 @@ export default {
books: 'Bücher',
authors: 'Autoren',
publishers: 'Verlage',
items: 'Artikel'
items: 'Artikel',
users: 'Benutzer',
logs: 'Logs'
},
books: {
title: 'Bücherübersicht',
@ -96,5 +98,33 @@ export default {
errorNoTitle: 'Bitte gib mindestens einen Titel an.',
updatedSuccess: 'Der Artikel wurde erfolgreich geupdated!',
insertedSuccess: 'Der Artikel wurde erfolgreich erstellt!'
},
logs: {
title: 'Logs',
gridColumns: {
user: 'Benutzer',
action: 'Aktion',
itemID: 'Item',
date: 'Datum'
},
logActions: [
'',
'Buch hinzugefügt',
'Buch geändert',
'Buch gelöscht',
'Author hinzugefügt',
'Author geändert',
'Author gelöscht',
'Verlag hinzugefügt',
'Verlag geändert',
'Verlag gelöscht',
'Artikel hinzugefügt',
'Artikel geändert',
'Artikel gelöscht',
'Benutzer hinzugefügt',
'Benutzer geändert',
'Benutzer gelöscht',
'Benutzerpasswort geändert'
]
}
}

View File

@ -36,7 +36,9 @@ export default {
books: 'Books',
authors: 'Authors',
publishers: 'Publishers',
items: 'Items'
items: 'Items',
users: 'Users',
logs: 'Logs'
},
books: {
title: 'Books overview',
@ -96,5 +98,33 @@ export default {
errorNoTitle: 'Please provide at least a title.',
updatedSuccess: 'The item was successfully updated!',
insertedSuccess: 'The item was successfully inserted!'
},
logs: {
title: 'Logs',
gridColumns: {
user: 'User',
action: 'Action',
itemID: 'Item',
date: 'Date'
},
logActions: [
'',
'Book added',
'Book updated',
'Book deleted',
'Author added',
'Author updated',
'Author deleted',
'Publisher added',
'Publisher updated',
'Publisher deleted',
'Item added',
'Item updated',
'Item deleted',
'User added',
'User updated',
'User deleted',
'Changed user password'
]
}
}

View File

@ -36,7 +36,9 @@ export default {
books: 'Livres',
authors: 'Auteurs',
publishers: 'Maisons d\'éditions',
items: 'Articles'
items: 'Articles',
users: 'Utilisateurs',
logs: 'Logs'
},
books: {
title: 'Liste des livres',
@ -98,6 +100,34 @@ export default {
errorNoTitle: 'Veuillez au moins saisir un titre.',
updatedSuccess: 'L\'article a éte mit a jour avec succès !',
insertedSuccess: 'L\'article a éte crée avec succès !'
},
logs: {
title: 'Logs',
gridColumns: {
user: 'Utilisateur',
action: 'Action',
itemID: 'Item',
date: 'Date'
},
logActions: [
'',
'Livre ajouté',
'Livre modifié',
'Livre supprimé',
'Auteur ajouté',
'Auteur modifié',
'Auteur summprimé',
'Maison d\'edition ajoutée',
'Maison d\'edition modifiée',
'Maison d\'edition supprimée',
'Article ajouté',
'Article modifié',
'Article supprimée',
'Utilisateur ajouté',
'Utilisateur modifié',
'Utilisateur supprimé',
'Mot de passe changé'
]
}
}

View File

@ -19,6 +19,8 @@ import 'vue-awesome/icons/refresh'
import 'vue-awesome/icons/sign-out'
import 'vue-awesome/icons/trash'
import 'vue-awesome/icons/edit'
import 'vue-awesome/icons/sort-asc'
import 'vue-awesome/icons/sort-desc'
import Icon from 'vue-awesome/components/Icon'
// Paginate import

View File

@ -14,6 +14,7 @@ import PublisherOverview from '@/components/PublisherOverview'
import Items from '@/components/Items'
import ItemsOverview from '@/components/ItemOverview'
import ItemsAddEdit from '@/components/ItemsAddEdit'
import Logs from '@/components/Logs'
Vue.use(Router)
@ -108,6 +109,11 @@ export default new Router({
path: '/items/:id/edit',
name: 'item-edit',
component: ItemsAddEdit
},
{
path: '/logs',
name: 'view-logs',
component: Logs
}
]
})

32
go.mod Normal file
View File

@ -0,0 +1,32 @@
module git.kolaente.de/konrad/Library
go 1.16
require (
github.com/davecgh/go-spew v1.1.1-0.20171005155431-ecdeabc65495 // indirect
github.com/denisenkom/go-mssqldb v0.10.0 // indirect
github.com/dgrijalva/jwt-go v3.0.1-0.20170608005149-a539ee1a749a+incompatible
github.com/go-ini/ini v1.28.2
github.com/go-sql-driver/mysql v1.3.1-0.20171007150158-ee359f95877b
github.com/go-xorm/builder v0.0.0-20170519032130-c8871c857d25 // indirect
github.com/go-xorm/core v0.5.7
github.com/go-xorm/xorm v0.6.4-0.20170930012613-29d4a0330a00
github.com/joho/godotenv v1.3.0 // indirect
github.com/labstack/echo v3.1.1-0.20170426170929-1049c9613cd3+incompatible
github.com/labstack/gommon v0.2.2-0.20170925052817-57409ada9da0 // indirect
github.com/lib/pq v1.10.1 // indirect
github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd // indirect
github.com/mattn/go-isatty v0.0.4-0.20170925054904-a5cdd64afdee // indirect
github.com/mattn/go-oci8 v0.1.1 // indirect
github.com/mattn/go-sqlite3 v1.5.0
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/smartystreets/goconvey v1.6.4 // indirect
github.com/stretchr/testify v1.2.1-0.20171231124224-87b1dfb5b2fa
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 // indirect
github.com/ziutek/mymysql v1.5.4 // indirect
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/testfixtures.v2 v2.4.3
gopkg.in/yaml.v2 v2.0.0 // indirect
)

72
go.sum Normal file
View File

@ -0,0 +1,72 @@
github.com/davecgh/go-spew v1.1.1-0.20171005155431-ecdeabc65495 h1:b2hEFhj0PgDc77eCeDUSKXynIoXJRt6yTZ8aMk2cPoI=
github.com/davecgh/go-spew v1.1.1-0.20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.10.0 h1:QykgLZBorFE95+gO3u9esLd0BmbvpWp0/waNNZfHBM8=
github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgrijalva/jwt-go v3.0.1-0.20170608005149-a539ee1a749a+incompatible h1:lwWnUpbS8H6DbUpe9VIY8G0vOupN8pnCPs68g9oxAJI=
github.com/dgrijalva/jwt-go v3.0.1-0.20170608005149-a539ee1a749a+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/go-ini/ini v1.28.2 h1:drmmYv7psRpoGZkPtPKKTB+ZFSnvmwCMfNj5o1nLh2Y=
github.com/go-ini/ini v1.28.2/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-sql-driver/mysql v1.3.1-0.20171007150158-ee359f95877b h1:U876wVumr5JIhbkg6n/bjYkgR2VwX9f/GjvHoz9tBsw=
github.com/go-sql-driver/mysql v1.3.1-0.20171007150158-ee359f95877b/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-xorm/builder v0.0.0-20170519032130-c8871c857d25 h1:jUX9yw6+iKrs/WuysV2M6ap/ObK/07SE/a7I2uxitwM=
github.com/go-xorm/builder v0.0.0-20170519032130-c8871c857d25/go.mod h1:M+P3wv0K2C+ynucGDEqJCeOTc+6DcAtiiqU8GrCksXY=
github.com/go-xorm/core v0.5.7 h1:ClaJQDjHDre5Yco2MmkWKniM8NNdC/OXmoy2HfxxECw=
github.com/go-xorm/core v0.5.7/go.mod h1:i7QESCABdFcvhgc8pdINtzlJf/6LC29if6ZJgHt9SHI=
github.com/go-xorm/xorm v0.6.4-0.20170930012613-29d4a0330a00 h1:sryNK0GCJOjs3WNgdCMjr7AuFrF4pYf9LrQcomTg7k8=
github.com/go-xorm/xorm v0.6.4-0.20170930012613-29d4a0330a00/go.mod h1:i7qRPD38xj/v75UV+a9pEzr5tfRaH2ndJfwt/fGbQhs=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo v3.1.1-0.20170426170929-1049c9613cd3+incompatible h1:mbe/VB+HbK7DFlOfYUlDOP9m28P64NFSqiHNpPu4h/Q=
github.com/labstack/echo v3.1.1-0.20170426170929-1049c9613cd3+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
github.com/labstack/gommon v0.2.2-0.20170925052817-57409ada9da0 h1:7AIW1qc9sYYTZLamTsRKSmVvJDXkZZrIWXHDK4Gq4X0=
github.com/labstack/gommon v0.2.2-0.20170925052817-57409ada9da0/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
github.com/lib/pq v1.10.1 h1:6VXZrLU0jHBYyAqrSPa+MgPfnSvTPuMgK+k0o5kVFWo=
github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd h1:Y4ZRx+RIPFlPL4gnD/I7bdqSNXHlNop1Q6NjQuHds00=
github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.4-0.20170925054904-a5cdd64afdee h1:L08yktFTj+MmaCAZBZKAU4EyW4hEDji2dPxLYJozx1s=
github.com/mattn/go-isatty v0.0.4-0.20170925054904-a5cdd64afdee/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-oci8 v0.1.1 h1:aEUDxNAyDG0tv8CA3TArnDQNyc4EhnWlsfxRgDHABHM=
github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI=
github.com/mattn/go-sqlite3 v1.5.0 h1:cD1JkMVOQgN+75Jni3VEkSwLkElfpfS194KbtOH9jX8=
github.com/mattn/go-sqlite3 v1.5.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/testify v1.2.1-0.20171231124224-87b1dfb5b2fa h1:umkGKiDLv+oYTelap19DADMEu2+JcsrhBnAydIELGAI=
github.com/stretchr/testify v1.2.1-0.20171231124224-87b1dfb5b2fa/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8=
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/testfixtures.v2 v2.4.3 h1:hURC7rEeqQPxZ2PUSscSYgUC5xAjd8NHOQumPxmILKo=
gopkg.in/testfixtures.v2 v2.4.3/go.mod h1:vyAq+MYCgNpR29qitQdLZhdbLFf4mR/2MFJRFoQZZ2M=
gopkg.in/yaml.v2 v2.0.0 h1:uUkhRGrsEyx/laRdeS6YIQKIys8pg+lRSRdVMTYjivs=
gopkg.in/yaml.v2 v2.0.0/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=

View File

@ -1,8 +1,8 @@
package main
import (
"git.mowie.cc/konrad/Library/models"
"git.mowie.cc/konrad/Library/routes"
"git.kolaente.de/konrad/Library/models"
"git.kolaente.de/konrad/Library/routes"
"context"
"fmt"

123
models/author_test.go Normal file
View File

@ -0,0 +1,123 @@
package models
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestAddOrUpdateAuthor(t *testing.T) {
// Create test database
assert.NoError(t, PrepareTestDatabase())
// TODO delete all existing authors from eventual previuous tests
// Get our doer
doer, exx, err := GetUserByID(1)
assert.True(t, exx)
assert.NoError(t, err)
// Bootstrap our test author
testauthor := Author{Forename: "test", Lastname: "tsting"}
// Create a new author
author1, err := AddOrUpdateAuthor(testauthor, &doer)
assert.NoError(t, err)
assert.Equal(t, testauthor.Forename, author1.Forename)
assert.Equal(t, testauthor.Lastname, author1.Lastname)
// And anotherone
author2, err := AddOrUpdateAuthor(testauthor, &doer)
assert.NoError(t, err)
assert.Equal(t, testauthor.Forename, author2.Forename)
assert.Equal(t, testauthor.Lastname, author2.Lastname)
// As of now, we should have 2 authors in total. Get the list and check.
allauthors, err := ListAuthors("")
assert.NoError(t, err)
for _, author := range allauthors {
assert.Equal(t, testauthor.Forename, author.Forename)
assert.Equal(t, testauthor.Lastname, author.Lastname)
}
// Should find something
allauthors, err = ListAuthors("tst")
assert.NoError(t, err)
for _, author := range allauthors {
assert.Equal(t, testauthor.Forename, author.Forename)
assert.Equal(t, testauthor.Lastname, author.Lastname)
}
// Get the new author
gotauthor, exists, err := GetAuthorByID(author1.ID)
assert.NoError(t, err)
assert.True(t, exists)
assert.Equal(t, gotauthor.Forename, testauthor.Forename)
assert.Equal(t, gotauthor.Lastname, testauthor.Lastname)
// Pass an empty author to see if it fails
_, err = AddOrUpdateAuthor(Author{}, &doer)
assert.Error(t, err)
assert.True(t, IsErrAuthorCannotBeEmpty(err))
// Update the author
testauthor.ID = author1.ID
testauthor.Forename = "Lorem Ipsum"
author1updated, err := AddOrUpdateAuthor(testauthor, &doer)
assert.NoError(t, err)
assert.Equal(t, testauthor.Forename, author1updated.Forename)
assert.Equal(t, testauthor.Lastname, author1updated.Lastname)
// Delete the author
err = DeleteAuthorByID(author1.ID, &doer)
assert.NoError(t, err)
// Check if it is gone
_, exists, err = GetAuthorByID(author1.ID)
assert.NoError(t, err)
assert.False(t, exists)
// Try deleting an author with ID = 0
err = DeleteAuthorByID(0, &doer)
assert.Error(t, err)
assert.True(t, IsErrIDCannotBeZero(err))
// =======================
// Testing without a table
// Drop the table to see it fail
x.DropTables(Author{})
// Test inserting
_, err = AddOrUpdateAuthor(Author{Forename: "ff", Lastname: "fff"}, &doer)
assert.Error(t, err)
// Test updating
_, err = AddOrUpdateAuthor(Author{ID: 3, Forename: "ff", Lastname: "fff"}, &doer)
assert.Error(t, err)
// Delete from some nonexistent
err = DeleteAuthorByID(3, &doer)
assert.Error(t, err)
// And get from nonexistant
_, err = ListAuthors("")
assert.Error(t, err)
//Aaaaaaaaaand recreate it
x.Sync(Author{})
}
func TestGetAuthorsByBook(t *testing.T) {
// Create test database
assert.NoError(t, PrepareTestDatabase())
// Drop the table to see it fail
x.DropTables(AuthorBook{})
_, err := GetAuthorsByBook(Book{ID: 1})
assert.Error(t, err)
//Aaaaaaaaaand recreate it
x.Sync(AuthorBook{})
}

View File

@ -1,27 +1,37 @@
package models
import "fmt"
// AddOrUpdateAuthor adds a new author based on an author struct
func AddOrUpdateAuthor(author Author) (newAuthor Author, err error) {
func AddOrUpdateAuthor(author Author, doer *User) (newAuthor Author, err error) {
// If the ID is 0, insert the author, otherwise update it
if author.ID == 0 {
// Check if the author is empty, only insert it if not
if author.Forename == "" && author.Lastname == "" {
return Author{}, fmt.Errorf("Author cannot be empty")
return Author{}, ErrAuthorCannotBeEmpty{}
}
_, err = x.Insert(&author)
if err != nil {
return Author{}, err
}
// Log
err = logAction(ActionTypeAuthorAdded, doer, author.ID)
if err != nil {
return Author{}, err
}
} else {
_, err = x.Where("id = ?", author.ID).Update(&author)
if err != nil {
return Author{}, err
}
// Log
err = logAction(ActionTypeAuthorUpdated, doer, author.ID)
if err != nil {
return Author{}, err
}
}
// Get the newly inserted author

View File

@ -1,12 +1,10 @@
package models
import "fmt"
// DeleteAuthorByID deletes an author by its ID
func DeleteAuthorByID(id int64) error {
func DeleteAuthorByID(id int64, doer *User) error {
// Check if the id is 0
if id == 0 {
return fmt.Errorf("ID cannot be 0")
return ErrIDCannotBeZero{}
}
// Delete the author
@ -18,6 +16,12 @@ func DeleteAuthorByID(id int64) error {
// Delete all book relations associated with that author
_, err = x.Delete(&AuthorBook{AuthorID: id})
if err != nil {
return err
}
// Logging
err = logAction(ActionTypeAuthorDeleted, doer, id)
return err
}

147
models/book_test.go Normal file
View File

@ -0,0 +1,147 @@
package models
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestAddOrUpdateBook(t *testing.T) {
// Create test database
assert.NoError(t, PrepareTestDatabase())
// Get our doer
doer, _, err := GetUserByID(1)
assert.NoError(t, err)
// Create a new author for testing purposes
testauthor1 := Author{Forename: "Testauthor wich", Lastname: "already exists"}
testauthorin1, err := AddOrUpdateAuthor(testauthor1, &doer)
assert.NoError(t, err)
// Bootstrap our test book
testbook := Book{
Title: "Test",
Description: "Lorem Ipsum",
Isbn: "9999999999-999-99",
Year: 2018,
Price: 9.99,
Status: 0,
Quantity: 10,
Publisher: Publisher{
Name: "TestPublisherWhich does not exist",
},
Authors: []Author{
{
Forename: "Test1",
Lastname: "Lorm",
},
{
Forename: "Test3",
Lastname: "Lorm",
},
{
ID: testauthorin1.ID,
},
},
}
// Insert one new Testbook
book1, err := AddOrUpdateBook(testbook, &doer)
assert.NoError(t, err)
// Check if everything was inserted correctly
assert.Equal(t, testbook.Title, book1.Title)
assert.Equal(t, testbook.Description, book1.Description)
assert.Equal(t, testbook.Isbn, book1.Isbn)
assert.Equal(t, testbook.Year, book1.Year)
assert.Equal(t, testbook.Price, book1.Price)
assert.Equal(t, testbook.Status, book1.Status)
assert.Equal(t, testbook.Quantity, book1.Quantity)
// Check if the publisher was inserted corectly
_, exists, err := GetPublisherByID(book1.Publisher.ID)
assert.NoError(t, err)
assert.True(t, exists)
// Check if the authors are there
assert.Equal(t, book1.Authors[0].Forename, testbook.Authors[0].Forename)
assert.Equal(t, book1.Authors[1].Forename, testbook.Authors[1].Forename)
assert.Equal(t, book1.Authors[2].Forename, testauthor1.Forename)
// And anotherone
book2, err := AddOrUpdateBook(testbook, &doer)
assert.NoError(t, err)
assert.Equal(t, testbook.Title, book2.Title) // If this works, the rest should work too so we don't need to recheck everythin again
// As of now, we should have 2 books in total. Get the list and check.
allbooks, err := ListBooks("")
assert.NoError(t, err)
for _, book := range allbooks {
assert.Equal(t, book.Title, testbook.Title)
}
// Search
allbooks, err = ListBooks("est")
assert.NoError(t, err)
for _, book := range allbooks {
assert.Equal(t, book.Title, testbook.Title)
}
// Get the new book
gotBook, exists, err := GetBookByID(book1.ID)
assert.NoError(t, err)
assert.True(t, exists)
assert.Equal(t, testbook.Title, gotBook.Title)
// Pass an empty Book to see if it fails
_, err = AddOrUpdateBook(Book{}, &doer)
assert.Error(t, err)
assert.True(t, IsErrBookTitleCannotBeEmpty(err))
// Update the book
testbook.ID = book1.ID
testbook.Title = "LormIspmus"
book1updated, err := AddOrUpdateBook(testbook, &doer)
assert.NoError(t, err)
assert.Equal(t, testbook.Title, book1updated.Title)
// Get the authors for that book
authorsbybook, err := GetAuthorsByBook(book1)
assert.NoError(t, err)
// Check if they are the right ones
// Yes I know, this is not a good way to do this. But as we get additional information when selecting from the database
// (ID, Created, Updated), this would fail if we'd just directly compared authorsbybook and testbook.Authors
assert.Equal(t, len(authorsbybook), len(testbook.Authors))
// Test Quantity
qty1, err := book1.getQuantity()
assert.NoError(t, err)
assert.Equal(t, book1.Quantity, qty1)
// Update the quantity and check again
err = book1.setQuantity(int64(99))
assert.NoError(t, err)
qty2, err := book1.getQuantity()
assert.NoError(t, err)
assert.Equal(t, int64(99), qty2)
// Delete the book
err = DeleteBookByID(book1.ID, &doer)
assert.NoError(t, err)
// Check if its gone
_, exists, err = GetBookByID(book1.ID)
assert.NoError(t, err)
assert.False(t, exists)
// Try deleting one with ID = 0
err = DeleteBookByID(0, &doer)
assert.Error(t, err)
assert.True(t, IsErrIDCannotBeZero(err))
}

View File

@ -1,7 +1,5 @@
package models
import "fmt"
/**
::USAGE::
@ -18,11 +16,11 @@ sie in die Datenbank eingetragen und mit dem Buch verknüpft.
*/
// AddOrUpdateBook adds a new book or updates an existing one, it takes a book struct with author and publisher. Inserts them if they don't already exist
func AddOrUpdateBook(book Book) (newBook Book, err error) {
func AddOrUpdateBook(book Book, doer *User) (newBook Book, err error) {
// Check if we have at least a booktitle when we're inserting a new book
if book.Title == "" && book.ID == 0 {
return Book{}, fmt.Errorf("the book should at least have a title")
if book.Title == "" {
return Book{}, ErrBookTitleCannotBeEmpty{}
}
// Take Publisher, check if it exists. If not, insert it
@ -45,7 +43,7 @@ func AddOrUpdateBook(book Book) (newBook Book, err error) {
book.PublisherID = publisherid
} else {
// Otherwise insert it and make it the new publisher afterwards
newPublisher, err := AddOrUpdatePublisher(Publisher{Name: book.Publisher.Name})
newPublisher, err := AddOrUpdatePublisher(Publisher{Name: book.Publisher.Name}, doer)
if err != nil {
return Book{}, err
}
@ -64,12 +62,24 @@ func AddOrUpdateBook(book Book) (newBook Book, err error) {
if err != nil {
return Book{}, err
}
// Log
err = logAction(ActionTypeBookAdded, doer, book.ID)
if err != nil {
return Book{}, err
}
} else {
// Update the book
_, err := x.Id(book.ID).Update(book)
if err != nil {
return Book{}, err
}
// Log
err = logAction(ActionTypeBookUpdated, doer, book.ID)
if err != nil {
return Book{}, err
}
}
// Set the Quantity
@ -93,8 +103,8 @@ func AddOrUpdateBook(book Book) (newBook Book, err error) {
if !exists {
// We have to insert authors on this inperformant way, because we need the ne ids afterwards
insertedAuthor, err := AddOrUpdateAuthor(author)
// We have to insert authors on this inperformant way, because we need the new ids afterwards
insertedAuthor, err := AddOrUpdateAuthor(author, doer)
if err != nil {
return Book{}, err

View File

@ -1,12 +1,10 @@
package models
import "fmt"
// DeleteBookByID deletes a book by its ID
func DeleteBookByID(id int64) error {
func DeleteBookByID(id int64, doer *User) error {
// Check if the id is 0
if id == 0 {
return fmt.Errorf("ID cannot be 0")
return ErrIDCannotBeZero{}
}
// Delete the book
@ -16,7 +14,7 @@ func DeleteBookByID(id int64) error {
return err
}
// Delete all authors associated with that book
// Delete all author associations for that book
_, err = x.Delete(&AuthorBook{BookID: id})
if err != nil {
return err
@ -24,6 +22,12 @@ func DeleteBookByID(id int64) error {
// Delete all quantites for this book
_, err = x.Delete(&Quantity{ItemID: id})
if err != nil {
return err
}
// Logging
err = logAction(ActionTypeBookDeleted, doer, id)
return err
}

77
models/config_test.go Normal file
View File

@ -0,0 +1,77 @@
package models
import (
"github.com/stretchr/testify/assert"
"io/ioutil"
"os"
"testing"
)
func TestSetConfig(t *testing.T) {
// Create test database
assert.NoError(t, PrepareTestDatabase())
// This should fail as it is looking for a nonexistent config
err := SetConfig()
assert.Error(t, err)
// Write an invalid config
configString := `[General
JWTSecret = Supersecret
Interface = ; This should make it automatically to :8080
[Database
Type = sqlite
Path = ./library.db
[User
Name = nope
Username = user
Passw]ord = 1234
Email = nope@none.com`
err = ioutil.WriteFile("config.ini", []byte(configString), 0644)
assert.NoError(t, err)
// Test setConfig (should fail as we're trying to parse an invalid config)
err = SetConfig()
assert.Error(t, err)
// Delete the invalid file
err = os.Remove("config.ini")
assert.NoError(t, err)
// Write a fake config
configString = `[General]
JWTSecret = Supersecret
Interface = ; This should make it automatically to :8080
[Database]
Type = sqlite
Path = ./library.db
[User]
Name = nope
Username = user
Password = 1234
Email = nope@none.com`
err = ioutil.WriteFile("config.ini", []byte(configString), 0644)
assert.NoError(t, err)
// Test setConfig
err = SetConfig()
assert.NoError(t, err)
// Check for the values
assert.Equal(t, []byte("Supersecret"), Config.JWTLoginSecret)
assert.Equal(t, string(":8080"), Config.Interface)
assert.Equal(t, string("sqlite"), Config.Database.Type)
assert.Equal(t, string("./library.db"), Config.Database.Path)
assert.Equal(t, string("nope"), Config.FirstUser.Name)
assert.Equal(t, string("user"), Config.FirstUser.Username)
assert.Equal(t, string("1234"), Config.FirstUser.Password)
assert.Equal(t, string("nope@none.com"), Config.FirstUser.Email)
// Remove the dummy config
err = os.Remove("config.ini")
assert.NoError(t, err)
}

177
models/error.go Normal file
View File

@ -0,0 +1,177 @@
package models
import "fmt"
// =====================
// User Operation Errors
// =====================
// ErrUsernameExists represents a "UsernameAlreadyExists" kind of error.
type ErrUsernameExists struct {
UserID int64
Username string
}
// IsErrUsernameExists checks if an error is a ErrUsernameExists.
func IsErrUsernameExists(err error) bool {
_, ok := err.(ErrUsernameExists)
return ok
}
func (err ErrUsernameExists) Error() string {
return fmt.Sprintf("a user with this username does already exist [user id: %d, username: %s]", err.UserID, err.Username)
}
// ErrUserEmailExists represents a "UserEmailExists" kind of error.
type ErrUserEmailExists struct {
UserID int64
Email string
}
// IsErrUserEmailExists checks if an error is a ErrUserEmailExists.
func IsErrUserEmailExists(err error) bool {
_, ok := err.(ErrUserEmailExists)
return ok
}
func (err ErrUserEmailExists) Error() string {
return fmt.Sprintf("a user with this email does already exist [user id: %d, email: %s]", err.UserID, err.Email)
}
// ErrNoUsername represents a "UsernameAlreadyExists" kind of error.
type ErrNoUsername struct {
UserID int64
}
// IsErrNoUsername checks if an error is a ErrUsernameExists.
func IsErrNoUsername(err error) bool {
_, ok := err.(ErrNoUsername)
return ok
}
func (err ErrNoUsername) Error() string {
return fmt.Sprintf("you need to specify a username [user id: %d]", err.UserID)
}
// ErrNoUsernamePassword represents a "NoUsernamePassword" kind of error.
type ErrNoUsernamePassword struct{}
// IsErrNoUsernamePassword checks if an error is a ErrNoUsernamePassword.
func IsErrNoUsernamePassword(err error) bool {
_, ok := err.(ErrNoUsernamePassword)
return ok
}
func (err ErrNoUsernamePassword) Error() string {
return fmt.Sprintf("you need to specify a username and a password")
}
// ErrUserDoesNotExist represents a "UserDoesNotExist" kind of error.
type ErrUserDoesNotExist struct {
UserID int64
}
// IsErrUserDoesNotExist checks if an error is a ErrUserDoesNotExist.
func IsErrUserDoesNotExist(err error) bool {
_, ok := err.(ErrUserDoesNotExist)
return ok
}
func (err ErrUserDoesNotExist) Error() string {
return fmt.Sprintf("this user does not exist [user id: %d]", err.UserID)
}
// ErrCouldNotGetUserID represents a "ErrCouldNotGetUserID" kind of error.
type ErrCouldNotGetUserID struct{}
// IsErrCouldNotGetUserID checks if an error is a ErrCouldNotGetUserID.
func IsErrCouldNotGetUserID(err error) bool {
_, ok := err.(ErrCouldNotGetUserID)
return ok
}
func (err ErrCouldNotGetUserID) Error() string {
return fmt.Sprintf("could not get user ID")
}
// ErrCannotDeleteLastUser represents a "ErrCannotDeleteLastUser" kind of error.
type ErrCannotDeleteLastUser struct{}
// IsErrCannotDeleteLastUser checks if an error is a ErrCannotDeleteLastUser.
func IsErrCannotDeleteLastUser(err error) bool {
_, ok := err.(ErrCannotDeleteLastUser)
return ok
}
func (err ErrCannotDeleteLastUser) Error() string {
return fmt.Sprintf("cannot delete last user")
}
// ===================
// Empty things errors
// ===================
// ErrIDCannotBeZero represents a "IDCannotBeZero" kind of error. Used if an ID (of something, not defined) is 0 where it should not.
type ErrIDCannotBeZero struct{}
// IsErrIDCannotBeZero checks if an error is a ErrIDCannotBeZero.
func IsErrIDCannotBeZero(err error) bool {
_, ok := err.(ErrIDCannotBeZero)
return ok
}
func (err ErrIDCannotBeZero) Error() string {
return fmt.Sprintf("ID cannot be 0")
}
// ErrAuthorCannotBeEmpty represents a "AuthorCannotBeEmpty" kind of error.
type ErrAuthorCannotBeEmpty struct{}
// IsErrAuthorCannotBeEmpty checks if an error is a ErrAuthorCannotBeEmpty.
func IsErrAuthorCannotBeEmpty(err error) bool {
_, ok := err.(ErrAuthorCannotBeEmpty)
return ok
}
func (err ErrAuthorCannotBeEmpty) Error() string {
return fmt.Sprintf("author cannot be empty")
}
// ErrItemTitleCannotBeEmpty represents a "ErrItemTitleCannotBeEmpty" kind of error.
type ErrItemTitleCannotBeEmpty struct{}
// IsErrItemTitleCannotBeEmpty checks if an error is a ErrItemTitleCannotBeEmpty.
func IsErrItemTitleCannotBeEmpty(err error) bool {
_, ok := err.(ErrItemTitleCannotBeEmpty)
return ok
}
func (err ErrItemTitleCannotBeEmpty) Error() string {
return fmt.Sprintf("title cannot be empty")
}
// ErrBookTitleCannotBeEmpty represents a "ErrBookTitleCannotBeEmpty" kind of error.
type ErrBookTitleCannotBeEmpty struct{}
// IsErrBookTitleCannotBeEmpty checks if an error is a ErrBookTitleCannotBeEmpty.
func IsErrBookTitleCannotBeEmpty(err error) bool {
_, ok := err.(ErrBookTitleCannotBeEmpty)
return ok
}
func (err ErrBookTitleCannotBeEmpty) Error() string {
return fmt.Sprintf("the book should at least have a title")
}
// ErrNoPublisherName represents a "ErrNoPublisherName" kind of error.
type ErrNoPublisherName struct{}
// IsErrNoPublisherName checks if an error is a ErrNoPublisherName.
func IsErrNoPublisherName(err error) bool {
_, ok := err.(ErrNoPublisherName)
return ok
}
func (err ErrNoPublisherName) Error() string {
return fmt.Sprintf("you need at least a name to insert a new publisher")
}

View File

@ -0,0 +1,7 @@
-
id: 1
name: 'John Doe'
username: 'user1'
password: '1234'
email: 'johndoe@example.com'
is_admin: true

109
models/item_test.go Normal file
View File

@ -0,0 +1,109 @@
package models
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestAddOrUpdateItem(t *testing.T) {
// Create test database
assert.NoError(t, PrepareTestDatabase())
// Get our doer
doer, _, err := GetUserByID(1)
assert.NoError(t, err)
// Bootstrap our test item
testitem := Item{
Title: "Testitem",
Price: 9.999,
Description: "Lorem Ipsum",
Other: "bs",
}
// Create a new item
item1, err := AddOrUpdateItem(testitem, &doer)
assert.NoError(t, err)
assert.Equal(t, testitem.Title, item1.Title)
assert.Equal(t, testitem.Price, item1.Price)
assert.Equal(t, testitem.Description, item1.Description)
assert.Equal(t, testitem.Other, item1.Other)
// And anotherone
item2, err := AddOrUpdateItem(testitem, &doer)
assert.NoError(t, err)
assert.Equal(t, testitem.Title, item2.Title)
// As of now, we should have 2 items in total. Get the list and check.
allitems, err := ListItems("")
assert.NoError(t, err)
for _, item := range allitems {
assert.Equal(t, testitem.Title, item.Title)
assert.Equal(t, testitem.Price, item.Price)
assert.Equal(t, testitem.Description, item.Description)
assert.Equal(t, testitem.Other, item.Other)
}
// Search
allitems, err = ListItems("esti")
assert.NoError(t, err)
for _, item := range allitems {
assert.Equal(t, testitem.Title, item.Title)
assert.Equal(t, testitem.Price, item.Price)
assert.Equal(t, testitem.Description, item.Description)
assert.Equal(t, testitem.Other, item.Other)
}
// Get the new item
gotitem, exists, err := GetItemByID(item1.ID)
assert.NoError(t, err)
assert.True(t, exists)
assert.Equal(t, testitem.Title, gotitem.Title)
assert.Equal(t, testitem.Price, gotitem.Price)
assert.Equal(t, testitem.Description, gotitem.Description)
assert.Equal(t, testitem.Other, gotitem.Other)
// Pass an empty item to see if it fails
_, err = AddOrUpdateItem(Item{}, &doer)
assert.Error(t, err)
assert.True(t, IsErrItemTitleCannotBeEmpty(err))
// Update the item
testitem.ID = item1.ID
testitem.Title = "Lorem Ipsum"
item1updated, err := AddOrUpdateItem(testitem, &doer)
assert.NoError(t, err)
assert.Equal(t, testitem.Title, item1updated.Title)
assert.Equal(t, testitem.Price, item1updated.Price)
assert.Equal(t, testitem.Description, item1updated.Description)
assert.Equal(t, testitem.Other, item1updated.Other)
// Test Quantity
qty1, err := item1.getQuantity()
assert.NoError(t, err)
assert.Equal(t, item1.Quantity, qty1)
// Update the quantity and check again
err = item1.setQuantity(int64(99))
assert.NoError(t, err)
qty2, err := item1.getQuantity()
assert.NoError(t, err)
assert.Equal(t, int64(99), qty2)
// Delete the item
err = DeleteItemByID(item1.ID, &doer)
assert.NoError(t, err)
// Check if it is gone
_, exists, err = GetItemByID(item1.ID)
assert.NoError(t, err)
assert.False(t, exists)
// Try deleting one with ID = 0
err = DeleteItemByID(0, &doer)
assert.Error(t, err)
assert.True(t, IsErrIDCannotBeZero(err))
}

View File

@ -1,17 +1,25 @@
package models
// AddOrUpdateItem adds or updates a item from a item struct
func AddOrUpdateItem(item Item) (newItem Item, err error) {
func AddOrUpdateItem(item Item, doer *User) (newItem Item, err error) {
// save the quantity for later use
qty := item.Quantity
if item.ID == 0 {
if item.Title != "" { // Only insert it if the title is not empty
_, err = x.Insert(&item)
if item.Title == "" { // Only insert it if the title is not empty
return Item{}, ErrItemTitleCannotBeEmpty{}
}
if err != nil {
return Item{}, err
}
_, err = x.Insert(&item)
if err != nil {
return Item{}, err
}
// Log
err = logAction(ActionTypeItemAdded, doer, item.ID)
if err != nil {
return Item{}, err
}
} else {
_, err = x.ID(item.ID).Update(&item)
@ -19,6 +27,12 @@ func AddOrUpdateItem(item Item) (newItem Item, err error) {
if err != nil {
return Item{}, err
}
// Log
err = logAction(ActionTypeItemUpdated, doer, item.ID)
if err != nil {
return Item{}, err
}
}
// Set the Quantity

View File

@ -1,12 +1,10 @@
package models
import "fmt"
// DeleteItemByID deletes a item by its ID
func DeleteItemByID(id int64) error {
func DeleteItemByID(id int64, doer *User) error {
// Check if the id is 0
if id == 0 {
return fmt.Errorf("ID cannot be 0")
return ErrIDCannotBeZero{}
}
// Delete the item
@ -18,6 +16,12 @@ func DeleteItemByID(id int64) error {
// Delete all quantites for this item
_, err = x.Delete(&Quantity{ItemID: id})
if err != nil {
return err
}
// Logging
err = logAction(ActionTypeItemDeleted, doer, id)
return err
}

41
models/log_action.go Normal file
View File

@ -0,0 +1,41 @@
package models
// ActionType is the action type
type ActionType int
// Define action types
const (
ActionTypeUnknown ActionType = -1
ActionTypeBookAdded ActionType = iota
ActionTypeBookUpdated
ActionTypeBookDeleted
ActionTypeAuthorAdded
ActionTypeAuthorUpdated
ActionTypeAuthorDeleted
ActionTypePublisherAdded
ActionTypePublisherUpdated
ActionTypePublisherDeleted
ActionTypeItemAdded
ActionTypeItemUpdated
ActionTypeItemDeleted
ActionTypeUserAdded
ActionTypeUserUpdated
ActionTypeUserDeleted
ActionTypeChangedUserPassword
)
// LogAction logs a user action
func logAction(actionType ActionType, user *User, itemID int64) (err error) {
_, err = x.Insert(UserLog{Log: actionType, UserID: user.ID, ItemID: itemID})
return
}
// GetAllLogs returns an array with all logs
func GetAllLogs() (logs []UserLog, err error) {
err = x.OrderBy("id DESC").Find(&logs)
if err != nil {
return logs, err
}
return logs, err
}

7
models/main_test.go Normal file
View File

@ -0,0 +1,7 @@
package models
import "testing"
func TestMain(m *testing.M) {
MainTest(m, "..")
}

View File

@ -53,21 +53,21 @@ func SetEngine() (err error) {
x.ShowSQL(Config.Database.ShowQueries)
// Check if the first user already exists, aka a user with the ID = 1. If not, insert it
_, exists, err := GetUserByID(1)
// Check if at least one user already exists. If not, insert it
total, err := x.Count(User{})
if err != nil {
return err
}
// If it doesn't exist, create it
if !exists {
_, err = CreateUser(Config.FirstUser)
if total < 1 {
Config.FirstUser.IsAdmin = true // Make the first user admin
_, err = CreateUser(Config.FirstUser, &User{ID: 0})
if err != nil {
// Janky hack, I know
if err.Error() != "this username is already taken. Please use another" {
return err
}
return err
}
fmt.Println("Created new user " + Config.FirstUser.Username)
}

12
models/models_test.go Normal file
View File

@ -0,0 +1,12 @@
package models
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestSetEngine(t *testing.T) {
Config.Database.Path = "file::memory:?cache=shared"
err := SetEngine()
assert.NoError(t, err)
}

78
models/publisher_test.go Normal file
View File

@ -0,0 +1,78 @@
package models
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestAddOrUpdatePublisher(t *testing.T) {
// Create test database
assert.NoError(t, PrepareTestDatabase())
// Get our doer
doer, _, err := GetUserByID(1)
assert.NoError(t, err)
// Bootstrap our test publisher
testpublisher := Publisher{
Name: "Testpublisher",
}
// Delete every prexisting publisher to have a fresh start
allpublishers, err := ListPublishers("")
assert.NoError(t, err)
for _, publisher := range allpublishers {
// Delete
err = DeletePublisherByID(publisher.ID, &doer)
assert.NoError(t, err)
// Check if it is gone
_, exists, err := GetPublisherByID(publisher.ID)
assert.NoError(t, err)
assert.False(t, exists)
}
// Create a new publisher
publisher1, err := AddOrUpdatePublisher(testpublisher, &doer)
assert.NoError(t, err)
assert.Equal(t, testpublisher.Name, publisher1.Name)
// Get the new publisher
gotpublisher, exists, err := GetPublisherByID(publisher1.ID)
assert.NoError(t, err)
assert.True(t, exists)
assert.Equal(t, testpublisher.Name, gotpublisher.Name)
// Pass an empty publisher to see if it fails
_, err = AddOrUpdatePublisher(Publisher{}, &doer)
assert.Error(t, err)
assert.True(t, IsErrNoPublisherName(err))
// Update the publisher
testpublisher.ID = publisher1.ID
testpublisher.Name = "Lorem Ipsum"
publisher1updated, err := AddOrUpdatePublisher(testpublisher, &doer)
assert.NoError(t, err)
assert.Equal(t, testpublisher.Name, publisher1updated.Name)
// Search
allpublishers, err = ListPublishers("rem")
assert.NoError(t, err)
assert.NotNil(t, allpublishers[0])
// Delete the publisher
err = DeletePublisherByID(publisher1.ID, &doer)
assert.NoError(t, err)
// Check if it is gone
_, exists, err = GetPublisherByID(publisher1.ID)
assert.NoError(t, err)
assert.False(t, exists)
// Try deleting one with ID = 0
err = DeletePublisherByID(0, &doer)
assert.Error(t, err)
assert.True(t, IsErrIDCannotBeZero(err))
}

View File

@ -1,18 +1,30 @@
package models
// AddOrUpdatePublisher adds or updates a publisher from a publisher struct
func AddOrUpdatePublisher(publisher Publisher) (newPublisher Publisher, err error) {
func AddOrUpdatePublisher(publisher Publisher, doer *User) (newPublisher Publisher, err error) {
if publisher.ID == 0 {
if publisher.Name != "" { // Only insert it if the name is not empty
_, err = x.Insert(&publisher)
if err != nil {
return Publisher{}, err
}
if publisher.Name == "" { // Only insert it if the name is not empty
return Publisher{}, ErrNoPublisherName{}
}
_, err = x.Insert(&publisher)
if err != nil {
return Publisher{}, err
}
// Log
err = logAction(ActionTypePublisherAdded, doer, publisher.ID)
if err != nil {
return Publisher{}, err
}
} else {
_, err = x.ID(publisher.ID).Update(&publisher)
if err != nil {
return Publisher{}, err
}
// Log
err = logAction(ActionTypePublisherUpdated, doer, publisher.ID)
if err != nil {
return Publisher{}, err
}

View File

@ -1,17 +1,14 @@
package models
import "fmt"
// DeletePublisherByID deletes a publisher by its ID
func DeletePublisherByID(id int64) error {
func DeletePublisherByID(id int64, doer *User) error {
// Check if the id is 0
if id == 0 {
return fmt.Errorf("ID cannot be 0")
return ErrIDCannotBeZero{}
}
// Delete the publisher
_, err := x.Id(id).Delete(&Publisher{})
if err != nil {
return err
}
@ -20,6 +17,12 @@ func DeletePublisherByID(id int64) error {
_, err = x.Table("books").
Where("publisher_id = ?", id).
Update(map[string]interface{}{"publisher_id": 0})
if err != nil {
return err
}
// Logging
err = logAction(ActionTypePublisherDeleted, doer, id)
return err
}

View File

@ -75,7 +75,7 @@ func (item Item) getQuantity() (quantity int64, err error) {
Select("quantities.id, quantity_relations.item_id, quantities.quantity, quantities.created").
Join("INNER", "quantity_relations", "quantities.item_id = quantity_relations.id").
Where("quantity_relations.item_id = ?", item.ID).
Desc("quantities.created").Get(&qty)
Desc("quantities.id").Get(&qty)
return qty.Quantity, err
}
@ -112,7 +112,7 @@ func (book Book) getQuantity() (quantity int64, err error) {
Select("quantities.id, quantity_relations.item_id, quantities.quantity, quantities.created").
Join("INNER", "quantity_relations", "quantities.item_id = quantity_relations.id").
Where("quantity_relations.book_id = ?", book.ID).
Desc("quantities.created").Get(&qty)
Desc("quantities.id").Get(&qty)
return qty.Quantity, err
}

29
models/quantity_test.go Normal file
View File

@ -0,0 +1,29 @@
package models
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestSetQuantity(t *testing.T) {
// Create test database
assert.NoError(t, PrepareTestDatabase())
// Set Quantity for a nonexistent item (should create)
err := SetQuantity(9999, 12)
assert.NoError(t, err)
// Check
qty, err := GetQuantity(9999)
assert.NoError(t, err)
assert.Equal(t, int64(12), qty)
// Update and check again
err = SetQuantity(9999, 120)
assert.NoError(t, err)
// Check
qty, err = GetQuantity(9999)
assert.NoError(t, err)
assert.Equal(t, int64(120), qty)
}

31
models/status_test.go Normal file
View File

@ -0,0 +1,31 @@
package models
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestGetStatusList(t *testing.T) {
// Create test database
assert.NoError(t, PrepareTestDatabase())
// Insert some dummy data
_, err := x.Insert(Status{Name: "new"})
assert.NoError(t, err)
_, err = x.Insert(Status{Name: "used"})
assert.NoError(t, err)
_, err = x.Insert(Status{Name: "other"})
assert.NoError(t, err)
// Get a status list
list, err := GetStatusList()
assert.NoError(t, err)
assert.Equal(t, "new", list[0].Name)
assert.Equal(t, "used", list[1].Name)
assert.Equal(t, "other", list[2].Name)
// Get a status by its ID
status, err := GetStatusByID(1)
assert.NoError(t, err)
assert.Equal(t, "new", status.Name)
}

19
models/test_fixtures.go Normal file
View File

@ -0,0 +1,19 @@
package models
import (
"gopkg.in/testfixtures.v2"
)
var fixtures *testfixtures.Context
// InitFixtures initialize test fixtures for a test database
func InitFixtures(helper testfixtures.Helper, dir string) (err error) {
testfixtures.SkipDatabaseNameCheck(true)
fixtures, err = testfixtures.NewFolder(x.DB().DB, helper, dir)
return err
}
// LoadFixtures load fixtures for a test database
func LoadFixtures() error {
return fixtures.Load()
}

57
models/unit_tests.go Normal file
View File

@ -0,0 +1,57 @@
package models
import (
"fmt"
"github.com/go-xorm/core"
"github.com/go-xorm/xorm"
"gopkg.in/testfixtures.v2"
"os"
"path/filepath"
"testing"
)
// MainTest creates the test engine
func MainTest(m *testing.M, pathToRoot string) {
var err error
fixturesDir := filepath.Join(pathToRoot, "models", "fixtures")
if err = createTestEngine(fixturesDir); err != nil {
fmt.Fprintf(os.Stderr, "Error creating test engine: %v\n", err)
os.Exit(1)
}
os.Exit(m.Run())
}
func createTestEngine(fixturesDir string) error {
var err error
x, err = xorm.NewEngine("sqlite3", "file::memory:?cache=shared")
//x, err = xorm.NewEngine("sqlite3", "db.db")
if err != nil {
return err
}
x.SetMapper(core.GonicMapper{})
// Sync dat shit
x.Sync(&Book{})
x.Sync(&User{})
x.Sync(&Publisher{})
x.Sync(&Author{})
x.Sync(&AuthorBook{})
x.Sync(&Status{})
x.Sync(&Quantity{})
x.Sync(&quantityRelation{})
x.Sync(&Item{})
x.Sync(&UserLog{})
// Show SQL-Queries if nessecary
if os.Getenv("UNIT_TESTS_VERBOSE") == "1" {
x.ShowSQL(true)
}
return InitFixtures(&testfixtures.SQLite{}, fixturesDir)
}
// PrepareTestDatabase load test fixtures into test database
func PrepareTestDatabase() error {
return LoadFixtures()
}

View File

@ -1,7 +1,6 @@
package models
import (
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/labstack/echo"
"golang.org/x/crypto/bcrypt"
@ -15,22 +14,23 @@ type UserLogin struct {
// User holds information about an user
type User struct {
ID int64 `xorm:"int(11) autoincr not null unique pk"`
Name string `xorm:"varchar(250)"`
Username string `xorm:"varchar(250) not null unique"`
Password string `xorm:"varchar(250) not null"`
Email string `xorm:"varchar(250)"`
Created int64 `xorm:"created"`
Updated int64 `xorm:"updated"`
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id"`
Name string `xorm:"varchar(250)" json:"name"`
Username string `xorm:"varchar(250) not null unique" json:"username"`
Password string `xorm:"varchar(250) not null" json:"password"`
Email string `xorm:"varchar(250)" json:"email"`
IsAdmin bool `xorm:"tinyint(1) not null" json:"isAdmin"`
Created int64 `xorm:"created" json:"created"`
Updated int64 `xorm:"updated" json:"updated"`
}
// UserLog logs user actions
type UserLog struct {
ID int64 `xorm:"int(11) autoincr not null unique pk"`
UserID int64 `xorm:"int(11)"`
Log string `xorm:"varchar(250)"`
ItemID int64 `xorm:"int(11)"`
Time int64 `xorm:"created"`
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id"`
UserID int64 `xorm:"int(11)" json:"userID"`
Log ActionType `xorm:"int(11)" json:"log"`
ItemID int64 `xorm:"int(11)" json:"itemID"`
Time int64 `xorm:"created" json:"time"`
}
// TableName returns the table name for users
@ -40,6 +40,11 @@ func (User) TableName() string {
// GetUserByID gets informations about a user by its ID
func GetUserByID(id int64) (user User, exists bool, err error) {
// Apparently xorm does otherwise look for all users but return only one, which leads to returing one even if the ID is 0
if id == 0 {
return User{}, false, nil
}
return GetUser(User{ID: id})
}
@ -47,61 +52,21 @@ func GetUserByID(id int64) (user User, exists bool, err error) {
func GetUser(user User) (userOut User, exists bool, err error) {
userOut = user
exists, err = x.Get(&userOut)
//fmt.Println(user, userOut, exists, err)
return userOut, exists, err
}
// CreateUser creates a new user and inserts it into the database
func CreateUser(user User) (newUser User, err error) {
newUser = user
// Check if we have all needed informations
if newUser.Password == "" || newUser.Username == "" {
return User{}, fmt.Errorf("you need to specify at least a username and a password")
}
// Check if the user already existst
_, exists, err := GetUser(User{Name: newUser.Name})
if err != nil {
return User{}, err
}
if exists {
return User{}, fmt.Errorf("this username is already taken. Please use another")
}
// Hash the password
newUser.Password, err = hashPassword(user.Password)
if err != nil {
return User{}, err
}
// Insert it
_, err = x.Insert(newUser)
if err != nil {
return User{}, err
}
return newUser, nil
}
// HashPassword hashes a password
func hashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}
// CheckUserCredentials checks user credentials
func CheckUserCredentials(u *UserLogin) (User, error) {
// Check if the user exists
var user = User{Username: u.Username}
exists, err := x.Get(&user)
user, exists, err := GetUser(User{Username: u.Username})
if err != nil {
return User{}, err
}
if !exists {
return User{}, fmt.Errorf("user does not exist")
return User{}, ErrUserDoesNotExist{}
}
// Check the users password
@ -120,7 +85,7 @@ func GetCurrentUser(c echo.Context) (user User, err error) {
claims := jwtinf.Claims.(jwt.MapClaims)
userID, ok := claims["id"].(float64)
if !ok {
return user, fmt.Errorf("Error getting UserID")
return user, ErrCouldNotGetUserID{}
}
user = User{
ID: int64(userID),
@ -133,18 +98,28 @@ func GetCurrentUser(c echo.Context) (user User, err error) {
}
// LogAction logs a user action
func logAction(action string, user User, itemID int64) (err error) {
_, err = x.Insert(UserLog{Log: action, UserID: user.ID, ItemID: itemID})
return
}
// LogAction logs a user action
func LogAction(action string, itemID int64, c echo.Context) (err error) {
func LogAction(actionType ActionType, itemID int64, c echo.Context) (err error) {
// Get the user options
user, err := GetCurrentUser(c)
if err != nil {
return err
}
return logAction(action, user, itemID)
return logAction(actionType, &user, itemID)
}
// IsAdmin checks based on it's JWT token if the user is admin
func IsAdmin(c echo.Context) bool {
// Get the users JWT token
jwtinf := c.Get("user").(*jwt.Token)
claims := jwtinf.Claims.(jwt.MapClaims)
// And check if he is admin
if claims["admin"].(bool) {
return true
}
// Send him to nirvarna if not
return false
}

134
models/user_add_update.go Normal file
View File

@ -0,0 +1,134 @@
package models
import (
"golang.org/x/crypto/bcrypt"
)
// CreateUser creates a new user and inserts it into the database
func CreateUser(user User, doer *User) (newUser User, err error) {
newUser = user
// Check if we have all needed informations
if newUser.Password == "" || newUser.Username == "" {
return User{}, ErrNoUsernamePassword{}
}
// Check if the user already existst with that username
existingUser, exists, err := GetUser(User{Username: newUser.Username})
if err != nil {
return User{}, err
}
if exists {
return User{}, ErrUsernameExists{existingUser.ID, existingUser.Username}
}
// Check if the user already existst with that username
existingUser, exists, err = GetUser(User{Email: newUser.Email})
if err != nil {
return User{}, err
}
if exists {
return User{}, ErrUserEmailExists{existingUser.ID, existingUser.Email}
}
// Hash the password
newUser.Password, err = hashPassword(user.Password)
if err != nil {
return User{}, err
}
// Insert it
_, err = x.Insert(newUser)
if err != nil {
return User{}, err
}
// Get the full new User
newUserOut, _, err := GetUser(newUser)
if err != nil {
return User{}, err
}
// Logging
err = logAction(ActionTypeUserAdded, doer, newUser.ID)
return newUserOut, err
}
// HashPassword hashes a password
func hashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}
// UpdateUser updates a user
func UpdateUser(user User, doer *User) (updatedUser User, err error) {
// Check if it exists
theUser, exists, err := GetUserByID(user.ID)
if err != nil {
return User{}, err
}
if exists {
// Check if we have at least a username
if user.Username == "" {
//return User{}, ErrNoUsername{user.ID}
user.Username = theUser.Username // Dont change the username if we dont have one
}
user.Password = theUser.Password // set the password to the one in the database to not accedently resetting it
// Update it
_, err = x.Id(user.ID).Update(user)
if err != nil {
return User{}, err
}
// Get the newly updated user
updatedUser, _, err = GetUserByID(user.ID)
if err != nil {
return User{}, err
}
// Logging
err = logAction(ActionTypeUserUpdated, doer, user.ID)
return updatedUser, err
}
return User{}, ErrUserDoesNotExist{user.ID}
}
// UpdateUserPassword updates the password of a user
func UpdateUserPassword(userID int64, newPassword string, doer *User) (err error) {
// Get all user details
user, exists, err := GetUserByID(userID)
if err != nil {
return err
}
if !exists {
return ErrUserDoesNotExist{userID}
}
// Hash the new password and set it
hashed, err := hashPassword(newPassword)
if err != nil {
return err
}
user.Password = hashed
// Update it
_, err = x.Id(user.ID).Update(user)
if err != nil {
return err
}
// Logging
err = logAction(ActionTypeChangedUserPassword, doer, user.ID)
return err
}

31
models/user_delete.go Normal file
View File

@ -0,0 +1,31 @@
package models
// DeleteUserByID deletes a user by its ID
func DeleteUserByID(id int64, doer *User) error {
// Check if the id is 0
if id == 0 {
return ErrIDCannotBeZero{}
}
// Check if there is > 1 user
total, err := x.Count(User{})
if err != nil {
return err
}
if total < 2 {
return ErrCannotDeleteLastUser{}
}
// Delete the user
_, err = x.Id(id).Delete(&User{})
if err != nil {
return err
}
// Logging
err = logAction(ActionTypeUserDeleted, doer, id)
return err
}

152
models/user_test.go Normal file
View File

@ -0,0 +1,152 @@
package models
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestCreateUser(t *testing.T) {
// Create test database
assert.NoError(t, PrepareTestDatabase())
// Get our doer
doer, _, err := GetUserByID(1)
assert.NoError(t, err)
// Our dummy user for testing
dummyuser := User{
Name: "noooem, dief",
Username: "testuu",
Password: "1234",
Email: "noone@example.com",
IsAdmin: true,
}
// Delete every preexisting user to have a fresh start
_, err = x.Where("1 = 1").Delete(&User{})
assert.NoError(t, err)
allusers, err := ListUsers("")
assert.NoError(t, err)
for _, user := range allusers {
// Delete it
err := DeleteUserByID(user.ID, &doer)
assert.NoError(t, err)
}
// Create a new user
createdUser, err := CreateUser(dummyuser, &doer)
assert.NoError(t, err)
// Create a second new user
createdUser2, err := CreateUser(User{Username: dummyuser.Username + "2", Email: dummyuser.Email + "m", Password: dummyuser.Password}, &doer)
assert.NoError(t, err)
// Check if it fails to create the same user again
_, err = CreateUser(dummyuser, &doer)
assert.Error(t, err)
// Check if it fails to create a user with just the same username
_, err = CreateUser(User{Username: dummyuser.Username, Password: "fsdf"}, &doer)
assert.Error(t, err)
assert.True(t, IsErrUsernameExists(err))
// Check if it fails to create one with the same email
_, err = CreateUser(User{Username: "noone", Password: "1234", Email: dummyuser.Email}, &doer)
assert.Error(t, err)
assert.True(t, IsErrUserEmailExists(err))
// Check if it fails to create a user without password and username
_, err = CreateUser(User{}, &doer)
assert.Error(t, err)
assert.True(t, IsErrNoUsernamePassword(err))
_, err = CreateUser(User{Name: "blub"}, &doer)
assert.Error(t, err)
assert.True(t, IsErrNoUsernamePassword(err))
// Check if he exists
theuser, exists, err := GetUser(createdUser)
assert.NoError(t, err)
assert.True(t, exists)
// Get by his ID
_, exists, err = GetUserByID(theuser.ID)
assert.NoError(t, err)
assert.True(t, exists)
// Passing 0 as ID should return an empty user
_, exists, err = GetUserByID(0)
assert.NoError(t, err)
assert.False(t, exists)
// Check the user credentials
user, err := CheckUserCredentials(&UserLogin{"testuu", "1234"})
assert.NoError(t, err)
assert.Equal(t, dummyuser.Name, user.Name)
// Check wrong password (should also fail)
_, err = CheckUserCredentials(&UserLogin{"testuu", "12345"})
assert.Error(t, err)
// Check usercredentials for a nonexistent user (should fail)
_, err = CheckUserCredentials(&UserLogin{"dfstestuu", "1234"})
assert.Error(t, err)
assert.True(t, IsErrUserDoesNotExist(err))
// Update the user
newname := "Test_te"
uuser, err := UpdateUser(User{ID: theuser.ID, Name: newname, Password: "444444"}, &doer)
assert.NoError(t, err)
assert.Equal(t, newname, uuser.Name)
assert.Equal(t, theuser.Password, uuser.Password) // Password should not change
assert.Equal(t, theuser.Username, uuser.Username) // Username should not change either
// Try updating one which does not exist
_, err = UpdateUser(User{ID: 99999, Username: "dg"}, &doer)
assert.Error(t, err)
assert.True(t, IsErrUserDoesNotExist(err))
// Update a users password
newpassword := "55555"
err = UpdateUserPassword(theuser.ID, newpassword, &doer)
assert.NoError(t, err)
// Check if it was changed
user, err = CheckUserCredentials(&UserLogin{theuser.Username, newpassword})
assert.NoError(t, err)
assert.Equal(t, newname, user.Name)
// Check if the searchterm works
all, err := ListUsers("test")
assert.NoError(t, err)
assert.True(t, len(all) > 0)
all, err = ListUsers("")
assert.NoError(t, err)
assert.True(t, len(all) > 0)
// Try updating the password of a nonexistent user (should fail)
err = UpdateUserPassword(9999, newpassword, &doer)
assert.Error(t, err)
assert.True(t, IsErrUserDoesNotExist(err))
// Delete it
err = DeleteUserByID(theuser.ID, &doer)
assert.NoError(t, err)
// Try deleting one with ID = 0
err = DeleteUserByID(0, &doer)
assert.Error(t, err)
assert.True(t, IsErrIDCannotBeZero(err))
// Try delete the last user (Should fail)
err = DeleteUserByID(createdUser2.ID, &doer)
assert.Error(t, err)
assert.True(t, IsErrCannotDeleteLastUser(err))
// Log some user action
err = logAction(ActionTypeUserUpdated, &doer, 1)
assert.NoError(t, err)
}

25
models/users_list.go Normal file
View File

@ -0,0 +1,25 @@
package models
// ListUsers returns a list with all users, filtered by an optional searchstring
func ListUsers(searchterm string) (users []User, err error) {
if searchterm == "" {
err = x.Find(&users)
} else {
err = x.
Where("username LIKE ?", "%"+searchterm+"%").
Or("name LIKE ?", "%"+searchterm+"%").
Find(&users)
}
// Obfuscate the password. Selecting everything except the password didn't work.
for i := range users {
users[i].Password = ""
}
if err != nil {
return []User{}, err
}
return users, nil
}

View File

@ -1,7 +1,7 @@
package v1
import (
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
"strconv"
@ -16,31 +16,34 @@ func AuthorDelete(c echo.Context) error {
authorID, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not get author infos"})
return c.JSON(http.StatusBadRequest, models.Message{"Author ID is invalid."})
}
// Check if the author exists
_, exists, err := models.GetAuthorByID(authorID)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could get author"})
return c.JSON(http.StatusInternalServerError, models.Message{"Could not get author."})
}
if !exists {
return c.JSON(http.StatusBadRequest, models.Message{"The author does not exist."})
return c.JSON(http.StatusNotFound, models.Message{"The author does not exist."})
}
// Get the user options
doer, err := models.GetCurrentUser(c)
if err != nil {
return err
}
// Delete it
err = models.DeleteAuthorByID(authorID)
err = models.DeleteAuthorByID(authorID, &doer)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not delete author"})
}
// Log the action
err = models.LogAction("Deleted an author", authorID, c)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not log"})
if models.IsErrIDCannotBeZero(err) {
return c.JSON(http.StatusBadRequest, models.Message{"Id cannot be 0"})
}
return c.JSON(http.StatusInternalServerError, models.Message{"Could not delete author."})
}
return c.JSON(http.StatusOK, models.Message{"success"})

View File

@ -1,7 +1,7 @@
package v1
import (
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
"strconv"
@ -18,7 +18,7 @@ func AuthorShow(c echo.Context) error {
// Make int
authorID, err := strconv.ParseInt(author, 10, 64)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Error getting author infos."})
return c.JSON(http.StatusBadRequest, models.Message{"Author ID is invalid."})
}
// Get Author Infos

View File

@ -2,7 +2,7 @@ package v1
import (
"encoding/json"
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
"strconv"
@ -17,7 +17,7 @@ func AuthorAddOrUpdate(c echo.Context) error {
if authorFromString == "" {
if err := c.Bind(&datAuthor); err != nil {
return c.JSON(http.StatusBadRequest, models.Message{"No author model provided"})
return c.JSON(http.StatusBadRequest, models.Message{"No author model provided."})
}
} else {
// Decode the JSON
@ -29,7 +29,7 @@ func AuthorAddOrUpdate(c echo.Context) error {
}
}
// Check if we have at least a Lastname
// Check if we have a name
if datAuthor.Lastname == "" && datAuthor.Forename == "" {
return c.JSON(http.StatusBadRequest, models.Message{"Please provide at least one name."})
}
@ -41,22 +41,37 @@ func AuthorAddOrUpdate(c echo.Context) error {
authorID, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not get book id"})
return c.JSON(http.StatusBadRequest, models.Message{"Invalid ID."})
}
datAuthor.ID = authorID
}
// Insert or update the author
newAuthor, err := models.AddOrUpdateAuthor(*datAuthor)
// Check if the author exists
if datAuthor.ID != 0 {
_, exists, err := models.GetAuthorByID(datAuthor.ID)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not check if the author exists."})
}
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Error"})
if !exists {
return c.JSON(http.StatusNotFound, models.Message{"The author does not exist."})
}
}
// Log the action
err = models.LogAction("Added or updated an author", newAuthor.ID, c)
// Get the user options
doer, err := models.GetCurrentUser(c)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not log"})
return err
}
// Insert or update the author
newAuthor, err := models.AddOrUpdateAuthor(*datAuthor, &doer)
if err != nil {
if models.IsErrAuthorCannotBeEmpty(err) {
return c.JSON(http.StatusBadRequest, models.Message{"Please provide at least a name and a lastname."})
}
return c.JSON(http.StatusInternalServerError, models.Message{"Error"})
}
return c.JSON(http.StatusOK, newAuthor)

View File

@ -1,7 +1,7 @@
package v1
import (
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
)
@ -15,7 +15,7 @@ func AuthorsList(c echo.Context) error {
list, err := models.ListAuthors(search)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Error getting authors"})
return c.JSON(http.StatusInternalServerError, models.Message{"Error getting authors."})
}
return c.JSON(http.StatusOK, list)

View File

@ -1,7 +1,7 @@
package v1
import (
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
"strconv"
@ -16,31 +16,34 @@ func BookDelete(c echo.Context) error {
bookID, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not get book infos"})
return c.JSON(http.StatusBadRequest, models.Message{"Book ID is invalid."})
}
// Check if the book exists
_, exists, err := models.GetBookByID(bookID)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could get book"})
return c.JSON(http.StatusInternalServerError, models.Message{"Could not get book."})
}
if !exists {
return c.JSON(http.StatusBadRequest, models.Message{"The book does not exist."})
return c.JSON(http.StatusNotFound, models.Message{"The book does not exist."})
}
// Get the user options
doer, err := models.GetCurrentUser(c)
if err != nil {
return err
}
// Delete it
err = models.DeleteBookByID(bookID)
err = models.DeleteBookByID(bookID, &doer)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not delete book"})
}
// Log the action
err = models.LogAction("Deleted a book", bookID, c)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not log"})
if models.IsErrIDCannotBeZero(err) {
return c.JSON(http.StatusBadRequest, models.Message{"Id cannot be 0"})
}
return c.JSON(http.StatusInternalServerError, models.Message{"Could not delete book."})
}
return c.JSON(http.StatusOK, models.Message{"success"})

View File

@ -1,7 +1,7 @@
package v1
import (
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
"strconv"
@ -17,12 +17,15 @@ func BookShow(c echo.Context) error {
// Make int
bookID, err := strconv.ParseInt(book, 10, 64)
if err != nil {
return c.JSON(http.StatusBadRequest, models.Message{"Book ID is invalid."})
}
// Get book infos
bookInfo, exists, err := models.GetBookByID(bookID)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not get book infos"})
return c.JSON(http.StatusInternalServerError, models.Message{"Could not get book infos."})
}
// Check if it exists

View File

@ -2,7 +2,7 @@ package v1
import (
"encoding/json"
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
"strconv"
@ -17,7 +17,7 @@ func BookAddOrUpdate(c echo.Context) error {
if bookFromString == "" {
if err := c.Bind(&datBook); err != nil {
return c.JSON(http.StatusBadRequest, models.Message{"No book model provided"})
return c.JSON(http.StatusBadRequest, models.Message{"No book model provided."})
}
} else {
// Decode the JSON
@ -25,7 +25,7 @@ func BookAddOrUpdate(c echo.Context) error {
err := dec.Decode(&datBook)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Error decoding book: " + err.Error()})
return c.JSON(http.StatusBadRequest, models.Message{"Error decoding book: " + err.Error()})
}
}
@ -36,28 +36,65 @@ func BookAddOrUpdate(c echo.Context) error {
bookID, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not get book id"})
return c.JSON(http.StatusBadRequest, models.Message{"Invalid ID."})
}
datBook.ID = bookID
}
// Check if the book exists
if datBook.ID != 0 {
_, exists, err := models.GetBookByID(datBook.ID)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not check if the book exists."})
}
if !exists {
return c.JSON(http.StatusNotFound, models.Message{"The book does not exist."})
}
}
// Check if we have at least a title
if datBook.Title == "" && datBook.ID == 0 {
return c.JSON(http.StatusBadRequest, models.Message{"You need at least a title to insert a new book!"})
}
// Get the user options
doer, err := models.GetCurrentUser(c)
if err != nil {
return err
}
// Insert or update the book
newBook, err := models.AddOrUpdateBook(*datBook)
newBook, err := models.AddOrUpdateBook(*datBook, &doer)
if err != nil {
if models.IsErrAuthorCannotBeEmpty(err) {
return c.JSON(http.StatusBadRequest, models.Message{"Id cannot be 0."})
}
if models.IsErrBookTitleCannotBeEmpty(err) {
return c.JSON(http.StatusBadRequest, models.Message{"You need to provide at least a title for the book."})
}
if models.IsErrNoPublisherName(err) {
return c.JSON(http.StatusBadRequest, models.Message{"You need to provide at least a name to insert a new publisher."})
}
return c.JSON(http.StatusInternalServerError, models.Message{"Error"})
}
// Log the action
err = models.LogAction("Added or updated a book", newBook.ID, c)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not log"})
}
/*if datBook.ID == 0 { // If the ID is, the author was added, otherwise updated
err = models.LogAction(models.ActionTypeBookAdded, newBook.ID, c)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not log."})
}
} else {
err = models.LogAction(models.ActionTypeBookUpdated, newBook.ID, c)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not log."})
}
}*/
return c.JSON(http.StatusOK, newBook)
}

View File

@ -5,7 +5,7 @@ import (
"net/http"
"fmt"
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
)
// BookList is the handler to list books
@ -18,7 +18,7 @@ func BookList(c echo.Context) error {
if err != nil {
fmt.Println(err)
return c.JSON(http.StatusInternalServerError, models.Message{"Error getting books"})
return c.JSON(http.StatusInternalServerError, models.Message{"Error getting books."})
}
return c.JSON(http.StatusOK, list)

View File

@ -2,7 +2,7 @@ package v1
import (
"encoding/json"
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
"strconv"
@ -17,7 +17,7 @@ func ItemAddOrUpdate(c echo.Context) error {
if itemFromString == "" {
if err := c.Bind(&datItem); err != nil {
return c.JSON(http.StatusBadRequest, models.Message{"No item model provided"})
return c.JSON(http.StatusBadRequest, models.Message{"No item model provided."})
}
} else {
// Decode the JSON
@ -25,7 +25,7 @@ func ItemAddOrUpdate(c echo.Context) error {
err := dec.Decode(&datItem)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Error decoding item: " + err.Error()})
return c.JSON(http.StatusBadRequest, models.Message{"Error decoding item: " + err.Error()})
}
}
@ -36,22 +36,37 @@ func ItemAddOrUpdate(c echo.Context) error {
itemID, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not get item id"})
return c.JSON(http.StatusBadRequest, models.Message{"Invalid ID."})
}
datItem.ID = itemID
}
// Insert or update the item
newItem, err := models.AddOrUpdateItem(*datItem)
// Check if the item exists
if datItem.ID != 0 {
_, exists, err := models.GetItemByID(datItem.ID)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not check if the item exists."})
}
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Error"})
if !exists {
return c.JSON(http.StatusNotFound, models.Message{"The item does not exist."})
}
}
// Log the action
err = models.LogAction("Added or updated an item", newItem.ID, c)
// Get the user options
doer, err := models.GetCurrentUser(c)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not log"})
return err
}
// Insert or update the item
newItem, err := models.AddOrUpdateItem(*datItem, &doer)
if err != nil {
if models.IsErrItemTitleCannotBeEmpty(err) {
return c.JSON(http.StatusInternalServerError, models.Message{"Please provide at least a title for the item."})
}
return c.JSON(http.StatusInternalServerError, models.Message{"Error"})
}
return c.JSON(http.StatusOK, newItem)

View File

@ -1,7 +1,7 @@
package v1
import (
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
"strconv"
@ -16,31 +16,34 @@ func ItemDelete(c echo.Context) error {
itemID, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not get item infos"})
return c.JSON(http.StatusBadRequest, models.Message{"Item ID is invalid."})
}
// Check if the item exists
_, exists, err := models.GetItemByID(itemID)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could get item"})
return c.JSON(http.StatusInternalServerError, models.Message{"Could not get item."})
}
if !exists {
return c.JSON(http.StatusBadRequest, models.Message{"The item does not exist."})
}
// Delete it
err = models.DeleteItemByID(itemID)
// Get the user options
doer, err := models.GetCurrentUser(c)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not delete item"})
return err
}
// Log the action
err = models.LogAction("Deleted an item", itemID, c)
// Delete it
err = models.DeleteItemByID(itemID, &doer)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not log"})
if models.IsErrIDCannotBeZero(err) {
return c.JSON(http.StatusBadRequest, models.Message{"Id cannot be 0"})
}
return c.JSON(http.StatusInternalServerError, models.Message{"Could not delete item."})
}
return c.JSON(http.StatusOK, models.Message{"success"})

View File

@ -1,7 +1,7 @@
package v1
import (
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
)
@ -15,7 +15,7 @@ func ItemsList(c echo.Context) error {
list, err := models.ListItems(search)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Error getting items"})
return c.JSON(http.StatusInternalServerError, models.Message{"Error getting items."})
}
return c.JSON(http.StatusOK, list)

View File

@ -1,7 +1,7 @@
package v1
import (
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
"strconv"
@ -18,7 +18,7 @@ func ItemShow(c echo.Context) error {
// Make int
itemID, err := strconv.ParseInt(item, 10, 64)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Error getting item infos."})
return c.JSON(http.StatusBadRequest, models.Message{"Item ID is invalid."})
}
// Get item Infos

25
routes/api/v1/logs.go Normal file
View File

@ -0,0 +1,25 @@
package v1
import (
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
)
// ShowLogs handels viewing logs
func ShowLogs(c echo.Context) error {
// Check if the user is admin
if !models.IsAdmin(c) {
return echo.ErrUnauthorized
}
// Get the logs
logs, err := models.GetAllLogs()
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Error getting logs."})
}
return c.JSON(http.StatusOK, logs)
}

View File

@ -2,7 +2,7 @@ package v1
import (
"encoding/json"
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
"strconv"
@ -18,7 +18,7 @@ func PublisherAddOrUpdate(c echo.Context) error {
if publisherFromString == "" {
// b := new(models.Publisher)
if err := c.Bind(&datPublisher); err != nil {
return c.JSON(http.StatusBadRequest, models.Message{"No publisher model provided"})
return c.JSON(http.StatusBadRequest, models.Message{"No publisher model provided."})
}
} else {
// Decode the JSON
@ -26,7 +26,7 @@ func PublisherAddOrUpdate(c echo.Context) error {
err := dec.Decode(&datPublisher)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Error decoding publisher: " + err.Error()})
return c.JSON(http.StatusBadRequest, models.Message{"Error decoding publisher: " + err.Error()})
}
}
@ -37,23 +37,52 @@ func PublisherAddOrUpdate(c echo.Context) error {
publisherID, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not get item id"})
return c.JSON(http.StatusBadRequest, models.Message{"Invalid ID."})
}
datPublisher.ID = publisherID
}
// Check if the publisher exists
if datPublisher.ID != 0 {
_, exists, err := models.GetPublisherByID(datPublisher.ID)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not check if the publisher exists."})
}
if !exists {
return c.JSON(http.StatusNotFound, models.Message{"The publisher does not exist."})
}
}
// Get the user options
doer, err := models.GetCurrentUser(c)
if err != nil {
return err
}
// Insert or update the publisher
newPublisher, err := models.AddOrUpdatePublisher(*datPublisher)
newPublisher, err := models.AddOrUpdatePublisher(*datPublisher, &doer)
if err != nil {
if models.IsErrNoPublisherName(err) {
return c.JSON(http.StatusBadRequest, models.Message{"You need to provide at least a name to insert a new publisher."})
}
return c.JSON(http.StatusInternalServerError, models.Message{"Error"})
}
// Log the action
err = models.LogAction("Added or updated a publisher", newPublisher.ID, c)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not log"})
}
/*if datPublisher.ID == 0 { // If the ID is, the author was added, otherwise updated
err = models.LogAction(models.ActionTypePublisherAdded, newPublisher.ID, c)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not log."})
}
} else {
err = models.LogAction(models.ActionTypePublisherUpdated, newPublisher.ID, c)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not log."})
}
}*/
return c.JSON(http.StatusOK, newPublisher)
}

View File

@ -1,7 +1,7 @@
package v1
import (
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
"strconv"
@ -16,32 +16,41 @@ func PublisherDelete(c echo.Context) error {
publisherID, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not get publisher infos"})
return c.JSON(http.StatusBadRequest, models.Message{"Publisher ID is invalid."})
}
// Check if the publisher exists
_, exists, err := models.GetPublisherByID(publisherID)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could get publisher"})
return c.JSON(http.StatusInternalServerError, models.Message{"Could not get publisher."})
}
if !exists {
return c.JSON(http.StatusBadRequest, models.Message{"The publisher does not exist."})
return c.JSON(http.StatusNotFound, models.Message{"The publisher does not exist."})
}
// Get the user options
doer, err := models.GetCurrentUser(c)
if err != nil {
return err
}
// Delete it
err = models.DeletePublisherByID(publisherID)
err = models.DeletePublisherByID(publisherID, &doer)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not delete publisher"})
if models.IsErrIDCannotBeZero(err) {
return c.JSON(http.StatusBadRequest, models.Message{"Id cannot be 0"})
}
return c.JSON(http.StatusInternalServerError, models.Message{"Could not delete publisher."})
}
// Log the action
err = models.LogAction("Deleted a publisher", publisherID, c)
/*err = models.LogAction(models.ActionTypePublisherDeleted, publisherID, c)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not log"})
}
return c.JSON(http.StatusInternalServerError, models.Message{"Could not log."})
}*/
return c.JSON(http.StatusOK, models.Message{"success"})
}

View File

@ -1,7 +1,7 @@
package v1
import (
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
)
@ -15,7 +15,7 @@ func PublishersList(c echo.Context) error {
list, err := models.ListPublishers(search)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Error getting publishers"})
return c.JSON(http.StatusInternalServerError, models.Message{"Error getting publishers."})
}
return c.JSON(http.StatusOK, list)

View File

@ -1,7 +1,7 @@
package v1
import (
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
"strconv"
@ -18,7 +18,7 @@ func PublisherShow(c echo.Context) error {
// Make int
publisherID, err := strconv.ParseInt(publisher, 10, 64)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Error getting publisher infos."})
return c.JSON(http.StatusBadRequest, models.Message{"Publisher ID is invalid."})
}
// Get Publisher Infos

View File

@ -1,7 +1,7 @@
package v1
import (
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
"strconv"

View File

@ -2,7 +2,7 @@ package v1
import (
"fmt"
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
"github.com/dgrijalva/jwt-go"
"github.com/labstack/echo"
)

View File

@ -0,0 +1,104 @@
package v1
import (
"encoding/json"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
"strconv"
"strings"
)
// UserAddOrUpdate is the handler to add a user
func UserAddOrUpdate(c echo.Context) error {
// Check if the user is admin
if !models.IsAdmin(c) {
return echo.ErrUnauthorized
}
// Check for Request Content
userFromString := c.FormValue("user")
var datUser *models.User
if userFromString == "" {
// b := new(models.User)
if err := c.Bind(&datUser); err != nil {
return c.JSON(http.StatusBadRequest, models.Message{"No user model provided."})
}
} else {
// Decode the JSON
dec := json.NewDecoder(strings.NewReader(userFromString))
err := dec.Decode(&datUser)
if err != nil {
return c.JSON(http.StatusBadRequest, models.Message{"Error decoding user: " + err.Error()})
}
}
// Check if we have an ID other than the one in the struct
id := c.Param("id")
if id != "" {
// Make int
userID, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return c.JSON(http.StatusBadRequest, models.Message{"Invalid ID."})
}
datUser.ID = userID
}
// Check if the user exists
_, exists, err := models.GetUserByID(datUser.ID)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not check if the user exists."})
}
// Get the doer options
doer, err := models.GetCurrentUser(c)
if err != nil {
return err
}
// Insert or update the user
var newUser models.User
if exists {
newUser, err = models.UpdateUser(*datUser, &doer)
} else {
newUser, err = models.CreateUser(*datUser, &doer)
}
if err != nil {
// Check for user already exists
if models.IsErrUsernameExists(err) {
return c.JSON(http.StatusBadRequest, models.Message{"A user with this username already exists."})
}
// Check for user with that email already exists
if models.IsErrUserEmailExists(err) {
return c.JSON(http.StatusBadRequest, models.Message{"A user with this email address already exists."})
}
// Check for no username provided
if models.IsErrNoUsername(err) {
return c.JSON(http.StatusBadRequest, models.Message{"Please specify a username."})
}
// Check for no username or password provided
if models.IsErrNoUsernamePassword(err) {
return c.JSON(http.StatusBadRequest, models.Message{"Please specify a username and a password."})
}
// Check for user does not exist
if models.IsErrUserDoesNotExist(err) {
return c.JSON(http.StatusBadRequest, models.Message{"The user does not exist."})
}
return c.JSON(http.StatusInternalServerError, models.Message{"Error"})
}
// Obfuscate his password
newUser.Password = ""
return c.JSON(http.StatusOK, newUser)
}

View File

@ -0,0 +1,60 @@
package v1
import (
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
"strconv"
)
// UserDelete is the handler to delete a user
func UserDelete(c echo.Context) error {
// Check if the user is admin
if !models.IsAdmin(c) {
return echo.ErrUnauthorized
}
id := c.Param("id")
// Make int
userID, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return c.JSON(http.StatusBadRequest, models.Message{"User ID is invalid."})
}
// Check if the user exists
_, exists, err := models.GetUserByID(userID)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not get user."})
}
if !exists {
return c.JSON(http.StatusNotFound, models.Message{"The user does not exist."})
}
// Get the doer options
doer, err := models.GetCurrentUser(c)
if err != nil {
return err
}
// Delete it
err = models.DeleteUserByID(userID, &doer)
if err != nil {
if models.IsErrIDCannotBeZero(err) {
return c.JSON(http.StatusBadRequest, models.Message{"Id cannot be 0"})
}
if models.IsErrCannotDeleteLastUser(err) {
return c.JSON(http.StatusBadRequest, models.Message{"Cannot delete last user."})
}
return c.JSON(http.StatusInternalServerError, models.Message{"Could not delete user."})
}
return c.JSON(http.StatusOK, models.Message{"success"})
}

View File

@ -0,0 +1,46 @@
package v1
import (
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
"strconv"
)
// UserShow gets all informations about a user
func UserShow(c echo.Context) error {
// Check if the user is admin
if !models.IsAdmin(c) {
return echo.ErrUnauthorized
}
user := c.Param("id")
if user == "" {
return c.JSON(http.StatusBadRequest, models.Message{"User ID cannot be empty."})
}
// Make int
userID, err := strconv.ParseInt(user, 10, 64)
if err != nil {
return c.JSON(http.StatusBadRequest, models.Message{"User ID is invalid."})
}
// Get User Infos
userInfos, exists, err := models.GetUserByID(userID)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Error getting user infos."})
}
// Check if it exists
if !exists {
return c.JSON(http.StatusNotFound, models.Message{"User not found."})
}
// Obfucate his password
userInfos.Password = ""
return c.JSON(http.StatusOK, userInfos)
}

View File

@ -0,0 +1,77 @@
package v1
import (
"net/http"
"strconv"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
)
type datPassword struct {
Password string `json:"password"`
}
// UserChangePassword is the handler to add a user
func UserChangePassword(c echo.Context) error {
// Get the ID
user := c.Param("id")
if user == "" {
return c.JSON(http.StatusBadRequest, models.Message{"User ID cannot be empty."})
}
// Make int
userID, err := strconv.ParseInt(user, 10, 64)
if err != nil {
return c.JSON(http.StatusBadRequest, models.Message{"User ID is invalid."})
}
// Check if the user is admin or itself
userJWTinfo, err := models.GetCurrentUser(c)
if !models.IsAdmin(c) {
if userJWTinfo.ID != userID {
return echo.ErrUnauthorized
}
}
// Check for Request Content
pwFromString := c.FormValue("password")
var datPw datPassword
if pwFromString == "" {
if err := c.Bind(&datPw); err != nil {
return c.JSON(http.StatusBadRequest, models.Message{"No password provided."})
}
} else {
// Take the value directly from the input
datPw.Password = pwFromString
}
// Get User Infos
_, exists, err := models.GetUserByID(userID)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Error getting user infos."})
}
// Check if it exists
if !exists {
return c.JSON(http.StatusNotFound, models.Message{"User not found."})
}
// Get the doer options
doer, err := models.GetCurrentUser(c)
if err != nil {
return err
}
err = models.UpdateUserPassword(userID, datPw.Password, &doer)
if err != nil {
return err
}
return c.JSON(http.StatusOK, models.Message{"The password was updated successfully"})
}

View File

@ -0,0 +1,28 @@
package v1
import (
"net/http"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
)
// UsersList lists all users
func UsersList(c echo.Context) error {
// Check if the user is admin
if !models.IsAdmin(c) {
return echo.ErrUnauthorized
}
// Prepare the searchterm
search := c.QueryParam("s")
list, err := models.ListUsers(search)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Error getting users."})
}
return c.JSON(http.StatusOK, list)
}

View File

@ -3,7 +3,7 @@ package routes
import (
"crypto/md5"
"encoding/hex"
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
"github.com/dgrijalva/jwt-go"
"github.com/labstack/echo"
"net/http"
@ -33,6 +33,7 @@ func Login(c echo.Context) error {
claims["username"] = user.Username
claims["email"] = user.Email
claims["id"] = user.ID
claims["admin"] = user.IsAdmin
claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
avatar := md5.Sum([]byte(user.Email))

View File

@ -4,8 +4,8 @@ import (
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
"git.mowie.cc/konrad/Library/models"
apiv1 "git.mowie.cc/konrad/Library/routes/api/v1"
"git.kolaente.de/konrad/Library/models"
apiv1 "git.kolaente.de/konrad/Library/routes/api/v1"
)
// NewEcho registers a new Echo instance
@ -58,6 +58,9 @@ func RegisterRoutes(e *echo.Echo) {
a.OPTIONS("/status/:id", SetCORSHeader)
a.OPTIONS("/items", SetCORSHeader)
a.OPTIONS("/items/:id", SetCORSHeader)
a.OPTIONS("/logs", SetCORSHeader)
a.OPTIONS("/users", SetCORSHeader)
a.OPTIONS("/users/:id", SetCORSHeader)
a.POST("/login", Login)
@ -106,7 +109,18 @@ func RegisterRoutes(e *echo.Echo) {
a.DELETE("/items/:id", apiv1.ItemDelete)
a.POST("/items/:id", apiv1.ItemAddOrUpdate)
// ====== Admin Routes ======
// Manage Users
a.GET("/users", apiv1.UsersList)
a.PUT("/users", apiv1.UserAddOrUpdate)
a.POST("/users/:id", apiv1.UserAddOrUpdate)
a.GET("/users/:id", apiv1.UserShow)
a.DELETE("/users/:id", apiv1.UserDelete)
a.POST("/users/:id/password", apiv1.UserChangePassword)
// View logs
a.GET("/logs", apiv1.ShowLogs)
/*
Alles nur mit Api machen, davor dann einen onepager mit vue.js.
@ -141,9 +155,10 @@ func RegisterRoutes(e *echo.Echo) {
GET /settings - |Nutzereinstellungen (Passwort, name etc)
POST /settings - |Nutzereinstellungen (Passwort, name etc)
GET /user - Nutzer anzeigen
PUT /user - |neue Nutzer anlegen
DELETE /user/:id - |nutzer löschen
POST /user/:id - |nutzer bearbeiten
GET /user - |Nutzer anzeigen --> Auch nur admin
PUT /user - |neue Nutzer anlegen --> Nur admin
DELETE /user/:id - |nutzer löschen --> Nur admins (sich selber löschen sollte nicht möglich sein)
POST /user/:id - |nutzer bearbeiten --> Sollte entweder Admin oder der Nutzer selbst sein
*/
}

15
systemd/library.service Normal file
View File

@ -0,0 +1,15 @@
[Unit]
Description=Library (A service to manage you library)
After=syslog.target
After=network.target
#After=mysqld.service
[Service]
RestartSec=2s
Type=simple
WorkingDirectory=/home/library
ExecStart=/home/library/library
Restart=always
[Install]
WantedBy=multi-user.target

15
vendor/github.com/davecgh/go-spew/LICENSE generated vendored Normal file
View File

@ -0,0 +1,15 @@
ISC License
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.
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.

152
vendor/github.com/davecgh/go-spew/spew/bypass.go generated vendored Normal file
View File

@ -0,0 +1,152 @@
// 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
// 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, 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.
// +build !js,!appengine,!safe,!disableunsafe
package spew
import (
"reflect"
"unsafe"
)
const (
// UnsafeDisabled is a build-time constant which specifies whether or
// not access to the unsafe package is available.
UnsafeDisabled = false
// ptrSize is the size of a pointer on the current arch.
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 = ptrSize
offsetScalar = uintptr(0)
offsetFlag = ptrSize * 2
// 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 = flagKindWidth - 1
flagRO = uintptr(1 << 0)
flagIndir = uintptr(1 << 1)
)
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
}
// 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
// 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
}
}
}
// unsafeReflectValue converts the passed reflect.Value into a one that bypasses
// the typical safety restrictions preventing access to unaddressable and
// unexported data. It works by digging the raw pointer to the underlying
// value out of the protected value and generating a new unprotected (unsafe)
// reflect.Value to it.
//
// 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)
}
}
pv := reflect.NewAt(vt, upv)
rv = pv
for i := 0; i < indirects; i++ {
rv = rv.Elem()
}
return rv
}

38
vendor/github.com/davecgh/go-spew/spew/bypasssafe.go generated vendored Normal file
View File

@ -0,0 +1,38 @@
// 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
// 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 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
package spew
import "reflect"
const (
// UnsafeDisabled is a build-time constant which specifies whether or
// not access to the unsafe package is available.
UnsafeDisabled = true
)
// unsafeReflectValue typically converts the passed reflect.Value into a one
// that bypasses the typical safety restrictions preventing access to
// unaddressable and unexported data. However, doing this relies on access to
// the unsafe package. This is a stub version which simply returns the passed
// reflect.Value when the unsafe package is not available.
func unsafeReflectValue(v reflect.Value) reflect.Value {
return v
}

341
vendor/github.com/davecgh/go-spew/spew/common.go generated vendored Normal file
View File

@ -0,0 +1,341 @@
/*
* 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
* 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
import (
"bytes"
"fmt"
"io"
"reflect"
"sort"
"strconv"
)
// Some constants in the form of bytes to avoid string overhead. This mirrors
// the technique used in the fmt package.
var (
panicBytes = []byte("(PANIC=")
plusBytes = []byte("+")
iBytes = []byte("i")
trueBytes = []byte("true")
falseBytes = []byte("false")
interfaceBytes = []byte("(interface {})")
commaNewlineBytes = []byte(",\n")
newlineBytes = []byte("\n")
openBraceBytes = []byte("{")
openBraceNewlineBytes = []byte("{\n")
closeBraceBytes = []byte("}")
asteriskBytes = []byte("*")
colonBytes = []byte(":")
colonSpaceBytes = []byte(": ")
openParenBytes = []byte("(")
closeParenBytes = []byte(")")
spaceBytes = []byte(" ")
pointerChainBytes = []byte("->")
nilAngleBytes = []byte("<nil>")
maxNewlineBytes = []byte("<max depth reached>\n")
maxShortBytes = []byte("<max>")
circularBytes = []byte("<already shown>")
circularShortBytes = []byte("<shown>")
invalidAngleBytes = []byte("<invalid>")
openBracketBytes = []byte("[")
closeBracketBytes = []byte("]")
percentBytes = []byte("%")
precisionBytes = []byte(".")
openAngleBytes = []byte("<")
closeAngleBytes = []byte(">")
openMapBytes = []byte("map[")
closeMapBytes = []byte("]")
lenEqualsBytes = []byte("len=")
capEqualsBytes = []byte("cap=")
)
// hexDigits is used to map a decimal value to a hex digit.
var hexDigits = "0123456789abcdef"
// catchPanic handles any panics that might occur during the handleMethods
// calls.
func catchPanic(w io.Writer, v reflect.Value) {
if err := recover(); err != nil {
w.Write(panicBytes)
fmt.Fprintf(w, "%v", err)
w.Write(closeParenBytes)
}
}
// handleMethods attempts to call the Error and String methods on the underlying
// type the passed reflect.Value represents and outputes the result to Writer w.
//
// It handles panics in any called methods by catching and displaying the error
// as the formatted value.
func handleMethods(cs *ConfigState, w io.Writer, v reflect.Value) (handled bool) {
// We need an interface to check if the type implements the error or
// Stringer interface. However, the reflect package won't give us an
// interface on certain things like unexported struct fields in order
// to enforce visibility rules. We use unsafe, when it's available,
// to bypass these restrictions since this package does not mutate the
// values.
if !v.CanInterface() {
if UnsafeDisabled {
return false
}
v = unsafeReflectValue(v)
}
// Choose whether or not to do error and Stringer interface lookups against
// the base type or a pointer to the base type depending on settings.
// Technically calling one of these methods with a pointer receiver can
// mutate the value, however, types which choose to satisify an error or
// Stringer interface with a pointer receiver should not be mutating their
// state inside these interface methods.
if !cs.DisablePointerMethods && !UnsafeDisabled && !v.CanAddr() {
v = unsafeReflectValue(v)
}
if v.CanAddr() {
v = v.Addr()
}
// Is it an error or Stringer?
switch iface := v.Interface().(type) {
case error:
defer catchPanic(w, v)
if cs.ContinueOnMethod {
w.Write(openParenBytes)
w.Write([]byte(iface.Error()))
w.Write(closeParenBytes)
w.Write(spaceBytes)
return false
}
w.Write([]byte(iface.Error()))
return true
case fmt.Stringer:
defer catchPanic(w, v)
if cs.ContinueOnMethod {
w.Write(openParenBytes)
w.Write([]byte(iface.String()))
w.Write(closeParenBytes)
w.Write(spaceBytes)
return false
}
w.Write([]byte(iface.String()))
return true
}
return false
}
// printBool outputs a boolean value as true or false to Writer w.
func printBool(w io.Writer, val bool) {
if val {
w.Write(trueBytes)
} else {
w.Write(falseBytes)
}
}
// printInt outputs a signed integer value to Writer w.
func printInt(w io.Writer, val int64, base int) {
w.Write([]byte(strconv.FormatInt(val, base)))
}
// printUint outputs an unsigned integer value to Writer w.
func printUint(w io.Writer, val uint64, base int) {
w.Write([]byte(strconv.FormatUint(val, base)))
}
// printFloat outputs a floating point value using the specified precision,
// which is expected to be 32 or 64bit, to Writer w.
func printFloat(w io.Writer, val float64, precision int) {
w.Write([]byte(strconv.FormatFloat(val, 'g', -1, precision)))
}
// printComplex outputs a complex value using the specified float precision
// for the real and imaginary parts to Writer w.
func printComplex(w io.Writer, c complex128, floatPrecision int) {
r := real(c)
w.Write(openParenBytes)
w.Write([]byte(strconv.FormatFloat(r, 'g', -1, floatPrecision)))
i := imag(c)
if i >= 0 {
w.Write(plusBytes)
}
w.Write([]byte(strconv.FormatFloat(i, 'g', -1, floatPrecision)))
w.Write(iBytes)
w.Write(closeParenBytes)
}
// printHexPtr outputs a uintptr formatted as hexadecimal with a leading '0x'
// prefix to Writer w.
func printHexPtr(w io.Writer, p uintptr) {
// Null pointer.
num := uint64(p)
if num == 0 {
w.Write(nilAngleBytes)
return
}
// Max uint64 is 16 bytes in hex + 2 bytes for '0x' prefix
buf := make([]byte, 18)
// It's simpler to construct the hex string right to left.
base := uint64(16)
i := len(buf) - 1
for num >= base {
buf[i] = hexDigits[num%base]
num /= base
i--
}
buf[i] = hexDigits[num]
// Add '0x' prefix.
i--
buf[i] = 'x'
i--
buf[i] = '0'
// Strip unused leading bytes.
buf = buf[i:]
w.Write(buf)
}
// valuesSorter implements sort.Interface to allow a slice of reflect.Value
// elements to be sorted.
type valuesSorter struct {
values []reflect.Value
strings []string // either nil or same len and values
cs *ConfigState
}
// newValuesSorter initializes a valuesSorter instance, which holds a set of
// surrogate keys on which the data should be sorted. It uses flags in
// ConfigState to decide if and how to populate those surrogate keys.
func newValuesSorter(values []reflect.Value, cs *ConfigState) sort.Interface {
vs := &valuesSorter{values: values, cs: cs}
if canSortSimply(vs.values[0].Kind()) {
return vs
}
if !cs.DisableMethods {
vs.strings = make([]string, len(values))
for i := range vs.values {
b := bytes.Buffer{}
if !handleMethods(cs, &b, vs.values[i]) {
vs.strings = nil
break
}
vs.strings[i] = b.String()
}
}
if vs.strings == nil && cs.SpewKeys {
vs.strings = make([]string, len(values))
for i := range vs.values {
vs.strings[i] = Sprintf("%#v", vs.values[i].Interface())
}
}
return vs
}
// canSortSimply tests whether a reflect.Kind is a primitive that can be sorted
// directly, or whether it should be considered for sorting by surrogate keys
// (if the ConfigState allows it).
func canSortSimply(kind reflect.Kind) bool {
// This switch parallels valueSortLess, except for the default case.
switch kind {
case reflect.Bool:
return true
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
return true
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
return true
case reflect.Float32, reflect.Float64:
return true
case reflect.String:
return true
case reflect.Uintptr:
return true
case reflect.Array:
return true
}
return false
}
// Len returns the number of values in the slice. It is part of the
// sort.Interface implementation.
func (s *valuesSorter) Len() int {
return len(s.values)
}
// Swap swaps the values at the passed indices. It is part of the
// sort.Interface implementation.
func (s *valuesSorter) Swap(i, j int) {
s.values[i], s.values[j] = s.values[j], s.values[i]
if s.strings != nil {
s.strings[i], s.strings[j] = s.strings[j], s.strings[i]
}
}
// valueSortLess returns whether the first value should sort before the second
// value. It is used by valueSorter.Less as part of the sort.Interface
// implementation.
func valueSortLess(a, b reflect.Value) bool {
switch a.Kind() {
case reflect.Bool:
return !a.Bool() && b.Bool()
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
return a.Int() < b.Int()
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
return a.Uint() < b.Uint()
case reflect.Float32, reflect.Float64:
return a.Float() < b.Float()
case reflect.String:
return a.String() < b.String()
case reflect.Uintptr:
return a.Uint() < b.Uint()
case reflect.Array:
// Compare the contents of both arrays.
l := a.Len()
for i := 0; i < l; i++ {
av := a.Index(i)
bv := b.Index(i)
if av.Interface() == bv.Interface() {
continue
}
return valueSortLess(av, bv)
}
}
return a.String() < b.String()
}
// Less returns whether the value at index i should sort before the
// value at index j. It is part of the sort.Interface implementation.
func (s *valuesSorter) Less(i, j int) bool {
if s.strings == nil {
return valueSortLess(s.values[i], s.values[j])
}
return s.strings[i] < s.strings[j]
}
// sortValues is a sort function that handles both native types and any type that
// can be converted to error or Stringer. Other inputs are sorted according to
// their Value.String() value to ensure display stability.
func sortValues(values []reflect.Value, cs *ConfigState) {
if len(values) == 0 {
return
}
sort.Sort(newValuesSorter(values, cs))
}

306
vendor/github.com/davecgh/go-spew/spew/config.go generated vendored Normal file
View File

@ -0,0 +1,306 @@
/*
* 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
* 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
import (
"bytes"
"fmt"
"io"
"os"
)
// ConfigState houses the configuration options used by spew to format and
// display values. There is a global instance, Config, that is used to control
// all top-level Formatter and Dump functionality. Each ConfigState instance
// provides methods equivalent to the top-level functions.
//
// The zero value for ConfigState provides no indentation. You would typically
// want to set it to a space or a tab.
//
// Alternatively, you can use NewDefaultConfig to get a ConfigState instance
// with default settings. See the documentation of NewDefaultConfig for default
// values.
type ConfigState struct {
// Indent specifies the string to use for each indentation level. The
// global config instance that all top-level functions use set this to a
// single space by default. If you would like more indentation, you might
// set this to a tab with "\t" or perhaps two spaces with " ".
Indent string
// MaxDepth controls the maximum number of levels to descend into nested
// data structures. The default, 0, means there is no limit.
//
// NOTE: Circular data structures are properly detected, so it is not
// necessary to set this value unless you specifically want to limit deeply
// nested data structures.
MaxDepth int
// DisableMethods specifies whether or not error and Stringer interfaces are
// invoked for types that implement them.
DisableMethods bool
// DisablePointerMethods specifies whether or not to check for and invoke
// error and Stringer interfaces on types which only accept a pointer
// receiver when the current type is not a pointer.
//
// NOTE: This might be an unsafe action since calling one of these methods
// with a pointer receiver could technically mutate the value, however,
// in practice, types which choose to satisify an error or Stringer
// interface with a pointer receiver should not be mutating their state
// 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 "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
// interface and return immediately instead of continuing to recurse into
// the internals of the data type.
//
// NOTE: This flag does not have any effect if method invocation is disabled
// via the DisableMethods or DisablePointerMethods options.
ContinueOnMethod bool
// SortKeys specifies map keys should be sorted before being printed. Use
// this to have a more deterministic, diffable output. Note that only
// native types (bool, int, uint, floats, uintptr and string) and types
// that support the error or Stringer interfaces (if methods are
// enabled) are supported, with other types sorted according to the
// reflect.Value.String() output which guarantees display stability.
SortKeys bool
// SpewKeys specifies that, as a last resort attempt, map keys should
// be spewed to strings and sorted by those strings. This is only
// considered if SortKeys is true.
SpewKeys bool
}
// Config is the active configuration of the top-level functions.
// The configuration can be changed by modifying the contents of spew.Config.
var Config = ConfigState{Indent: " "}
// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the formatted string as a value that satisfies error. See NewFormatter
// for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Errorf(format, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Errorf(format string, a ...interface{}) (err error) {
return fmt.Errorf(format, c.convertArgs(a)...)
}
// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Fprint(w, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Fprint(w io.Writer, a ...interface{}) (n int, err error) {
return fmt.Fprint(w, c.convertArgs(a)...)
}
// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Fprintf(w, format, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
return fmt.Fprintf(w, format, c.convertArgs(a)...)
}
// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it
// passed with a Formatter interface returned by c.NewFormatter. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Fprintln(w, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
return fmt.Fprintln(w, c.convertArgs(a)...)
}
// Print is a wrapper for fmt.Print that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Print(c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Print(a ...interface{}) (n int, err error) {
return fmt.Print(c.convertArgs(a)...)
}
// Printf is a wrapper for fmt.Printf that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Printf(format, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Printf(format string, a ...interface{}) (n int, err error) {
return fmt.Printf(format, c.convertArgs(a)...)
}
// Println is a wrapper for fmt.Println that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Println(c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Println(a ...interface{}) (n int, err error) {
return fmt.Println(c.convertArgs(a)...)
}
// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the resulting string. See NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Sprint(c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Sprint(a ...interface{}) string {
return fmt.Sprint(c.convertArgs(a)...)
}
// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the resulting string. See NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Sprintf(format, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Sprintf(format string, a ...interface{}) string {
return fmt.Sprintf(format, c.convertArgs(a)...)
}
// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it
// were passed with a Formatter interface returned by c.NewFormatter. It
// returns the resulting string. See NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Sprintln(c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Sprintln(a ...interface{}) string {
return fmt.Sprintln(c.convertArgs(a)...)
}
/*
NewFormatter returns a custom formatter that satisfies the fmt.Formatter
interface. As a result, it integrates cleanly with standard fmt package
printing functions. The formatter is useful for inline printing of smaller data
types similar to the standard %v format specifier.
The custom formatter only responds to the %v (most compact), %+v (adds pointer
addresses), %#v (adds types), and %#+v (adds types and pointer addresses) verb
combinations. Any other verbs such as %x and %q will be sent to the the
standard fmt package for formatting. In addition, the custom formatter ignores
the width and precision arguments (however they will still work on the format
specifiers not handled by the custom formatter).
Typically this function shouldn't be called directly. It is much easier to make
use of the custom formatter by calling one of the convenience functions such as
c.Printf, c.Println, or c.Printf.
*/
func (c *ConfigState) NewFormatter(v interface{}) fmt.Formatter {
return newFormatter(c, v)
}
// Fdump formats and displays the passed arguments to io.Writer w. It formats
// exactly the same as Dump.
func (c *ConfigState) Fdump(w io.Writer, a ...interface{}) {
fdump(c, w, a...)
}
/*
Dump displays the passed parameters to standard out with newlines, customizable
indentation, and additional debug information such as complete types and all
pointer addresses used to indirect to the final value. It provides the
following features over the built-in printing facilities provided by the fmt
package:
* Pointers are dereferenced and followed
* Circular data structures are detected and handled properly
* Custom Stringer/error interfaces are optionally invoked, including
on unexported types
* Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer
variables
* Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output
The configuration options are controlled by modifying the public members
of c. See ConfigState for options documentation.
See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to
get the formatted result as a string.
*/
func (c *ConfigState) Dump(a ...interface{}) {
fdump(c, os.Stdout, a...)
}
// Sdump returns a string with the passed arguments formatted exactly the same
// as Dump.
func (c *ConfigState) Sdump(a ...interface{}) string {
var buf bytes.Buffer
fdump(c, &buf, a...)
return buf.String()
}
// convertArgs accepts a slice of arguments and returns a slice of the same
// length with each argument converted to a spew Formatter interface using
// the ConfigState associated with s.
func (c *ConfigState) convertArgs(args []interface{}) (formatters []interface{}) {
formatters = make([]interface{}, len(args))
for index, arg := range args {
formatters[index] = newFormatter(c, arg)
}
return formatters
}
// NewDefaultConfig returns a ConfigState with the following default settings.
//
// Indent: " "
// MaxDepth: 0
// DisableMethods: false
// DisablePointerMethods: false
// ContinueOnMethod: false
// SortKeys: false
func NewDefaultConfig() *ConfigState {
return &ConfigState{Indent: " "}
}

211
vendor/github.com/davecgh/go-spew/spew/doc.go generated vendored Normal file
View File

@ -0,0 +1,211 @@
/*
* 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
* 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 implements a deep pretty printer for Go data structures to aid in
debugging.
A quick overview of the additional features spew provides over the built-in
printing facilities for Go data types are as follows:
* Pointers are dereferenced and followed
* Circular data structures are detected and handled properly
* Custom Stringer/error interfaces are optionally invoked, including
on unexported types
* Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer
variables
* Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output (only when using
Dump style)
There are two different approaches spew allows for dumping Go data structures:
* Dump style which prints with newlines, customizable indentation,
and additional debug information such as types and all pointer addresses
used to indirect to the final value
* A custom Formatter interface that integrates cleanly with the standard fmt
package and replaces %v, %+v, %#v, and %#+v to provide inline printing
similar to the default %v while providing the additional functionality
outlined above and passing unsupported format verbs such as %x and %q
along to fmt
Quick Start
This section demonstrates how to quickly get started with spew. See the
sections below for further details on formatting and configuration options.
To dump a variable with full newlines, indentation, type, and pointer
information use Dump, Fdump, or Sdump:
spew.Dump(myVar1, myVar2, ...)
spew.Fdump(someWriter, myVar1, myVar2, ...)
str := spew.Sdump(myVar1, myVar2, ...)
Alternatively, if you would prefer to use format strings with a compacted inline
printing style, use the convenience wrappers Printf, Fprintf, etc with
%v (most compact), %+v (adds pointer addresses), %#v (adds types), or
%#+v (adds types and pointer addresses):
spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
Configuration Options
Configuration of spew is handled by fields in the ConfigState type. For
convenience, all of the top-level functions use a global state available
via the spew.Config global.
It is also possible to create a ConfigState instance that provides methods
equivalent to the top-level functions. This allows concurrent configuration
options. See the ConfigState documentation for more details.
The following configuration options are available:
* Indent
String to use for each indentation level for Dump functions.
It is a single space by default. A popular alternative is "\t".
* MaxDepth
Maximum number of levels to descend into nested data structures.
There is no limit by default.
* DisableMethods
Disables invocation of error and Stringer interface methods.
Method invocation is enabled by default.
* DisablePointerMethods
Disables invocation of error and Stringer interface methods on types
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.
* SortKeys
Specifies map keys should be sorted before being printed. Use
this to have a more deterministic, diffable output. Note that
only native types (bool, int, uint, floats, uintptr and string)
and types which implement error or Stringer interfaces are
supported with other types sorted according to the
reflect.Value.String() output which guarantees display
stability. Natural map order is used by default.
* SpewKeys
Specifies that, as a last resort attempt, map keys should be
spewed to strings and sorted by those strings. This is only
considered if SortKeys is true.
Dump Usage
Simply call spew.Dump with a list of variables you want to dump:
spew.Dump(myVar1, myVar2, ...)
You may also call spew.Fdump if you would prefer to output to an arbitrary
io.Writer. For example, to dump to standard error:
spew.Fdump(os.Stderr, myVar1, myVar2, ...)
A third option is to call spew.Sdump to get the formatted output as a string:
str := spew.Sdump(myVar1, myVar2, ...)
Sample Dump Output
See the Dump example for details on the setup of the types and variables being
shown here.
(main.Foo) {
unexportedField: (*main.Bar)(0xf84002e210)({
flag: (main.Flag) flagTwo,
data: (uintptr) <nil>
}),
ExportedField: (map[interface {}]interface {}) (len=1) {
(string) (len=3) "one": (bool) true
}
}
Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C
command as shown.
([]uint8) (len=32 cap=32) {
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|
}
Custom Formatter
Spew provides a custom formatter that implements the fmt.Formatter interface
so that it integrates cleanly with standard fmt package printing functions. The
formatter is useful for inline printing of smaller data types similar to the
standard %v format specifier.
The custom formatter only responds to the %v (most compact), %+v (adds pointer
addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb
combinations. Any other verbs such as %x and %q will be sent to the the
standard fmt package for formatting. In addition, the custom formatter ignores
the width and precision arguments (however they will still work on the format
specifiers not handled by the custom formatter).
Custom Formatter Usage
The simplest way to make use of the spew custom formatter is to call one of the
convenience functions such as spew.Printf, spew.Println, or spew.Printf. The
functions have syntax you are most likely already familiar with:
spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
spew.Println(myVar, myVar2)
spew.Fprintf(os.Stderr, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
spew.Fprintf(os.Stderr, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
See the Index for the full list convenience functions.
Sample Formatter Output
Double pointer to a uint8:
%v: <**>5
%+v: <**>(0xf8400420d0->0xf8400420c8)5
%#v: (**uint8)5
%#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5
Pointer to circular struct with a uint8 field and a pointer to itself:
%v: <*>{1 <*><shown>}
%+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)<shown>}
%#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)<shown>}
%#+v: (*main.circular)(0xf84003e260){ui8:(uint8)1 c:(*main.circular)(0xf84003e260)<shown>}
See the Printf example for details on the setup of variables being shown
here.
Errors
Since it is possible for custom Stringer/error interfaces to panic, spew
detects them and handles them internally by printing the panic information
inline with the output. Since spew is intended to provide deep pretty printing
capabilities on structures, it intentionally does not return any errors.
*/
package spew

509
vendor/github.com/davecgh/go-spew/spew/dump.go generated vendored Normal file
View File

@ -0,0 +1,509 @@
/*
* 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
* 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
import (
"bytes"
"encoding/hex"
"fmt"
"io"
"os"
"reflect"
"regexp"
"strconv"
"strings"
)
var (
// uint8Type is a reflect.Type representing a uint8. It is used to
// convert cgo types to uint8 slices for hexdumping.
uint8Type = reflect.TypeOf(uint8(0))
// 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$`)
// 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$`)
// 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$`)
)
// dumpState contains information about the state of a dump operation.
type dumpState struct {
w io.Writer
depth int
pointers map[uintptr]int
ignoreNextType bool
ignoreNextIndent bool
cs *ConfigState
}
// indent performs indentation according to the depth level and cs.Indent
// option.
func (d *dumpState) indent() {
if d.ignoreNextIndent {
d.ignoreNextIndent = false
return
}
d.w.Write(bytes.Repeat([]byte(d.cs.Indent), d.depth))
}
// unpackValue returns values inside of non-nil interfaces when possible.
// This is useful for data types like structs, arrays, slices, and maps which
// can contain varying types packed inside an interface.
func (d *dumpState) unpackValue(v reflect.Value) reflect.Value {
if v.Kind() == reflect.Interface && !v.IsNil() {
v = v.Elem()
}
return v
}
// dumpPtr handles formatting of pointers by indirecting them as necessary.
func (d *dumpState) dumpPtr(v reflect.Value) {
// Remove pointers at or below the current depth from map used to detect
// circular refs.
for k, depth := range d.pointers {
if depth >= d.depth {
delete(d.pointers, k)
}
}
// Keep list of all dereferenced pointers to show later.
pointerChain := make([]uintptr, 0)
// Figure out how many levels of indirection there are by dereferencing
// pointers and unpacking interfaces down the chain while detecting circular
// references.
nilFound := false
cycleFound := false
indirects := 0
ve := v
for ve.Kind() == reflect.Ptr {
if ve.IsNil() {
nilFound = true
break
}
indirects++
addr := ve.Pointer()
pointerChain = append(pointerChain, addr)
if pd, ok := d.pointers[addr]; ok && pd < d.depth {
cycleFound = true
indirects--
break
}
d.pointers[addr] = d.depth
ve = ve.Elem()
if ve.Kind() == reflect.Interface {
if ve.IsNil() {
nilFound = true
break
}
ve = ve.Elem()
}
}
// Display type information.
d.w.Write(openParenBytes)
d.w.Write(bytes.Repeat(asteriskBytes, indirects))
d.w.Write([]byte(ve.Type().String()))
d.w.Write(closeParenBytes)
// Display pointer information.
if !d.cs.DisablePointerAddresses && len(pointerChain) > 0 {
d.w.Write(openParenBytes)
for i, addr := range pointerChain {
if i > 0 {
d.w.Write(pointerChainBytes)
}
printHexPtr(d.w, addr)
}
d.w.Write(closeParenBytes)
}
// Display dereferenced value.
d.w.Write(openParenBytes)
switch {
case nilFound:
d.w.Write(nilAngleBytes)
case cycleFound:
d.w.Write(circularBytes)
default:
d.ignoreNextType = true
d.dump(ve)
}
d.w.Write(closeParenBytes)
}
// dumpSlice handles formatting of arrays and slices. Byte (uint8 under
// reflection) arrays and slices are dumped in hexdump -C fashion.
func (d *dumpState) dumpSlice(v reflect.Value) {
// Determine whether this type should be hex dumped or not. Also,
// for types which should be hexdumped, try to use the underlying data
// first, then fall back to trying to convert them to a uint8 slice.
var buf []uint8
doConvert := false
doHexDump := false
numEntries := v.Len()
if numEntries > 0 {
vt := v.Index(0).Type()
vts := vt.String()
switch {
// C types that need to be converted.
case cCharRE.MatchString(vts):
fallthrough
case cUnsignedCharRE.MatchString(vts):
fallthrough
case cUint8tCharRE.MatchString(vts):
doConvert = true
// Try to use existing uint8 slices and fall back to converting
// and copying if that fails.
case vt.Kind() == reflect.Uint8:
// We need an addressable interface to convert the type
// to a byte slice. However, the reflect package won't
// give us an interface on certain things like
// unexported struct fields in order to enforce
// visibility rules. We use unsafe, when available, to
// bypass these restrictions since this package does not
// mutate the values.
vs := v
if !vs.CanInterface() || !vs.CanAddr() {
vs = unsafeReflectValue(vs)
}
if !UnsafeDisabled {
vs = vs.Slice(0, numEntries)
// Use the existing uint8 slice if it can be
// type asserted.
iface := vs.Interface()
if slice, ok := iface.([]uint8); ok {
buf = slice
doHexDump = true
break
}
}
// The underlying data needs to be converted if it can't
// be type asserted to a uint8 slice.
doConvert = true
}
// Copy and convert the underlying type if needed.
if doConvert && vt.ConvertibleTo(uint8Type) {
// Convert and copy each element into a uint8 byte
// slice.
buf = make([]uint8, numEntries)
for i := 0; i < numEntries; i++ {
vv := v.Index(i)
buf[i] = uint8(vv.Convert(uint8Type).Uint())
}
doHexDump = true
}
}
// Hexdump the entire slice as needed.
if doHexDump {
indent := strings.Repeat(d.cs.Indent, d.depth)
str := indent + hex.Dump(buf)
str = strings.Replace(str, "\n", "\n"+indent, -1)
str = strings.TrimRight(str, d.cs.Indent)
d.w.Write([]byte(str))
return
}
// Recursively call dump for each item.
for i := 0; i < numEntries; i++ {
d.dump(d.unpackValue(v.Index(i)))
if i < (numEntries - 1) {
d.w.Write(commaNewlineBytes)
} else {
d.w.Write(newlineBytes)
}
}
}
// dump is the main workhorse for dumping a value. It uses the passed reflect
// value to figure out what kind of object we are dealing with and formats it
// appropriately. It is a recursive function, however circular data structures
// are detected and handled properly.
func (d *dumpState) dump(v reflect.Value) {
// Handle invalid reflect values immediately.
kind := v.Kind()
if kind == reflect.Invalid {
d.w.Write(invalidAngleBytes)
return
}
// Handle pointers specially.
if kind == reflect.Ptr {
d.indent()
d.dumpPtr(v)
return
}
// Print type information unless already handled elsewhere.
if !d.ignoreNextType {
d.indent()
d.w.Write(openParenBytes)
d.w.Write([]byte(v.Type().String()))
d.w.Write(closeParenBytes)
d.w.Write(spaceBytes)
}
d.ignoreNextType = false
// Display length and capacity if the built-in len and cap functions
// work with the value's kind and the len/cap itself is non-zero.
valueLen, valueCap := 0, 0
switch v.Kind() {
case reflect.Array, reflect.Slice, reflect.Chan:
valueLen, valueCap = v.Len(), v.Cap()
case reflect.Map, reflect.String:
valueLen = v.Len()
}
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 !d.cs.DisableCapacities && valueCap != 0 {
if valueLen != 0 {
d.w.Write(spaceBytes)
}
d.w.Write(capEqualsBytes)
printInt(d.w, int64(valueCap), 10)
}
d.w.Write(closeParenBytes)
d.w.Write(spaceBytes)
}
// Call Stringer/error interfaces if they exist and the handle methods flag
// is enabled
if !d.cs.DisableMethods {
if (kind != reflect.Invalid) && (kind != reflect.Interface) {
if handled := handleMethods(d.cs, d.w, v); handled {
return
}
}
}
switch kind {
case reflect.Invalid:
// Do nothing. We should never get here since invalid has already
// been handled above.
case reflect.Bool:
printBool(d.w, v.Bool())
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
printInt(d.w, v.Int(), 10)
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
printUint(d.w, v.Uint(), 10)
case reflect.Float32:
printFloat(d.w, v.Float(), 32)
case reflect.Float64:
printFloat(d.w, v.Float(), 64)
case reflect.Complex64:
printComplex(d.w, v.Complex(), 32)
case reflect.Complex128:
printComplex(d.w, v.Complex(), 64)
case reflect.Slice:
if v.IsNil() {
d.w.Write(nilAngleBytes)
break
}
fallthrough
case reflect.Array:
d.w.Write(openBraceNewlineBytes)
d.depth++
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
d.indent()
d.w.Write(maxNewlineBytes)
} else {
d.dumpSlice(v)
}
d.depth--
d.indent()
d.w.Write(closeBraceBytes)
case reflect.String:
d.w.Write([]byte(strconv.Quote(v.String())))
case reflect.Interface:
// The only time we should get here is for nil interfaces due to
// unpackValue calls.
if v.IsNil() {
d.w.Write(nilAngleBytes)
}
case reflect.Ptr:
// Do nothing. We should never get here since pointers have already
// been handled above.
case reflect.Map:
// nil maps should be indicated as different than empty maps
if v.IsNil() {
d.w.Write(nilAngleBytes)
break
}
d.w.Write(openBraceNewlineBytes)
d.depth++
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
d.indent()
d.w.Write(maxNewlineBytes)
} else {
numEntries := v.Len()
keys := v.MapKeys()
if d.cs.SortKeys {
sortValues(keys, d.cs)
}
for i, key := range keys {
d.dump(d.unpackValue(key))
d.w.Write(colonSpaceBytes)
d.ignoreNextIndent = true
d.dump(d.unpackValue(v.MapIndex(key)))
if i < (numEntries - 1) {
d.w.Write(commaNewlineBytes)
} else {
d.w.Write(newlineBytes)
}
}
}
d.depth--
d.indent()
d.w.Write(closeBraceBytes)
case reflect.Struct:
d.w.Write(openBraceNewlineBytes)
d.depth++
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
d.indent()
d.w.Write(maxNewlineBytes)
} else {
vt := v.Type()
numFields := v.NumField()
for i := 0; i < numFields; i++ {
d.indent()
vtf := vt.Field(i)
d.w.Write([]byte(vtf.Name))
d.w.Write(colonSpaceBytes)
d.ignoreNextIndent = true
d.dump(d.unpackValue(v.Field(i)))
if i < (numFields - 1) {
d.w.Write(commaNewlineBytes)
} else {
d.w.Write(newlineBytes)
}
}
}
d.depth--
d.indent()
d.w.Write(closeBraceBytes)
case reflect.Uintptr:
printHexPtr(d.w, uintptr(v.Uint()))
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
printHexPtr(d.w, v.Pointer())
// There were not any other types at the time this code was written, but
// fall back to letting the default fmt package handle it in case any new
// types are added.
default:
if v.CanInterface() {
fmt.Fprintf(d.w, "%v", v.Interface())
} else {
fmt.Fprintf(d.w, "%v", v.String())
}
}
}
// fdump is a helper function to consolidate the logic from the various public
// methods which take varying writers and config states.
func fdump(cs *ConfigState, w io.Writer, a ...interface{}) {
for _, arg := range a {
if arg == nil {
w.Write(interfaceBytes)
w.Write(spaceBytes)
w.Write(nilAngleBytes)
w.Write(newlineBytes)
continue
}
d := dumpState{w: w, cs: cs}
d.pointers = make(map[uintptr]int)
d.dump(reflect.ValueOf(arg))
d.w.Write(newlineBytes)
}
}
// Fdump formats and displays the passed arguments to io.Writer w. It formats
// exactly the same as Dump.
func Fdump(w io.Writer, a ...interface{}) {
fdump(&Config, w, a...)
}
// Sdump returns a string with the passed arguments formatted exactly the same
// as Dump.
func Sdump(a ...interface{}) string {
var buf bytes.Buffer
fdump(&Config, &buf, a...)
return buf.String()
}
/*
Dump displays the passed parameters to standard out with newlines, customizable
indentation, and additional debug information such as complete types and all
pointer addresses used to indirect to the final value. It provides the
following features over the built-in printing facilities provided by the fmt
package:
* Pointers are dereferenced and followed
* Circular data structures are detected and handled properly
* Custom Stringer/error interfaces are optionally invoked, including
on unexported types
* Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer
variables
* Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output
The configuration options are controlled by an exported package global,
spew.Config. See ConfigState for options documentation.
See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to
get the formatted result as a string.
*/
func Dump(a ...interface{}) {
fdump(&Config, os.Stdout, a...)
}

419
vendor/github.com/davecgh/go-spew/spew/format.go generated vendored Normal file
View File

@ -0,0 +1,419 @@
/*
* 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
* 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
import (
"bytes"
"fmt"
"reflect"
"strconv"
"strings"
)
// supportedFlags is a list of all the character flags supported by fmt package.
const supportedFlags = "0-+# "
// formatState implements the fmt.Formatter interface and contains information
// about the state of a formatting operation. The NewFormatter function can
// be used to get a new Formatter which can be used directly as arguments
// in standard fmt package printing calls.
type formatState struct {
value interface{}
fs fmt.State
depth int
pointers map[uintptr]int
ignoreNextType bool
cs *ConfigState
}
// buildDefaultFormat recreates the original format string without precision
// and width information to pass in to fmt.Sprintf in the case of an
// unrecognized type. Unless new types are added to the language, this
// function won't ever be called.
func (f *formatState) buildDefaultFormat() (format string) {
buf := bytes.NewBuffer(percentBytes)
for _, flag := range supportedFlags {
if f.fs.Flag(int(flag)) {
buf.WriteRune(flag)
}
}
buf.WriteRune('v')
format = buf.String()
return format
}
// constructOrigFormat recreates the original format string including precision
// and width information to pass along to the standard fmt package. This allows
// automatic deferral of all format strings this package doesn't support.
func (f *formatState) constructOrigFormat(verb rune) (format string) {
buf := bytes.NewBuffer(percentBytes)
for _, flag := range supportedFlags {
if f.fs.Flag(int(flag)) {
buf.WriteRune(flag)
}
}
if width, ok := f.fs.Width(); ok {
buf.WriteString(strconv.Itoa(width))
}
if precision, ok := f.fs.Precision(); ok {
buf.Write(precisionBytes)
buf.WriteString(strconv.Itoa(precision))
}
buf.WriteRune(verb)
format = buf.String()
return format
}
// unpackValue returns values inside of non-nil interfaces when possible and
// ensures that types for values which have been unpacked from an interface
// are displayed when the show types flag is also set.
// This is useful for data types like structs, arrays, slices, and maps which
// can contain varying types packed inside an interface.
func (f *formatState) unpackValue(v reflect.Value) reflect.Value {
if v.Kind() == reflect.Interface {
f.ignoreNextType = false
if !v.IsNil() {
v = v.Elem()
}
}
return v
}
// formatPtr handles formatting of pointers by indirecting them as necessary.
func (f *formatState) formatPtr(v reflect.Value) {
// Display nil if top level pointer is nil.
showTypes := f.fs.Flag('#')
if v.IsNil() && (!showTypes || f.ignoreNextType) {
f.fs.Write(nilAngleBytes)
return
}
// Remove pointers at or below the current depth from map used to detect
// circular refs.
for k, depth := range f.pointers {
if depth >= f.depth {
delete(f.pointers, k)
}
}
// Keep list of all dereferenced pointers to possibly show later.
pointerChain := make([]uintptr, 0)
// Figure out how many levels of indirection there are by derferencing
// pointers and unpacking interfaces down the chain while detecting circular
// references.
nilFound := false
cycleFound := false
indirects := 0
ve := v
for ve.Kind() == reflect.Ptr {
if ve.IsNil() {
nilFound = true
break
}
indirects++
addr := ve.Pointer()
pointerChain = append(pointerChain, addr)
if pd, ok := f.pointers[addr]; ok && pd < f.depth {
cycleFound = true
indirects--
break
}
f.pointers[addr] = f.depth
ve = ve.Elem()
if ve.Kind() == reflect.Interface {
if ve.IsNil() {
nilFound = true
break
}
ve = ve.Elem()
}
}
// Display type or indirection level depending on flags.
if showTypes && !f.ignoreNextType {
f.fs.Write(openParenBytes)
f.fs.Write(bytes.Repeat(asteriskBytes, indirects))
f.fs.Write([]byte(ve.Type().String()))
f.fs.Write(closeParenBytes)
} else {
if nilFound || cycleFound {
indirects += strings.Count(ve.Type().String(), "*")
}
f.fs.Write(openAngleBytes)
f.fs.Write([]byte(strings.Repeat("*", indirects)))
f.fs.Write(closeAngleBytes)
}
// Display pointer information depending on flags.
if f.fs.Flag('+') && (len(pointerChain) > 0) {
f.fs.Write(openParenBytes)
for i, addr := range pointerChain {
if i > 0 {
f.fs.Write(pointerChainBytes)
}
printHexPtr(f.fs, addr)
}
f.fs.Write(closeParenBytes)
}
// Display dereferenced value.
switch {
case nilFound:
f.fs.Write(nilAngleBytes)
case cycleFound:
f.fs.Write(circularShortBytes)
default:
f.ignoreNextType = true
f.format(ve)
}
}
// format is the main workhorse for providing the Formatter interface. It
// uses the passed reflect value to figure out what kind of object we are
// dealing with and formats it appropriately. It is a recursive function,
// however circular data structures are detected and handled properly.
func (f *formatState) format(v reflect.Value) {
// Handle invalid reflect values immediately.
kind := v.Kind()
if kind == reflect.Invalid {
f.fs.Write(invalidAngleBytes)
return
}
// Handle pointers specially.
if kind == reflect.Ptr {
f.formatPtr(v)
return
}
// Print type information unless already handled elsewhere.
if !f.ignoreNextType && f.fs.Flag('#') {
f.fs.Write(openParenBytes)
f.fs.Write([]byte(v.Type().String()))
f.fs.Write(closeParenBytes)
}
f.ignoreNextType = false
// Call Stringer/error interfaces if they exist and the handle methods
// flag is enabled.
if !f.cs.DisableMethods {
if (kind != reflect.Invalid) && (kind != reflect.Interface) {
if handled := handleMethods(f.cs, f.fs, v); handled {
return
}
}
}
switch kind {
case reflect.Invalid:
// Do nothing. We should never get here since invalid has already
// been handled above.
case reflect.Bool:
printBool(f.fs, v.Bool())
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
printInt(f.fs, v.Int(), 10)
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
printUint(f.fs, v.Uint(), 10)
case reflect.Float32:
printFloat(f.fs, v.Float(), 32)
case reflect.Float64:
printFloat(f.fs, v.Float(), 64)
case reflect.Complex64:
printComplex(f.fs, v.Complex(), 32)
case reflect.Complex128:
printComplex(f.fs, v.Complex(), 64)
case reflect.Slice:
if v.IsNil() {
f.fs.Write(nilAngleBytes)
break
}
fallthrough
case reflect.Array:
f.fs.Write(openBracketBytes)
f.depth++
if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
f.fs.Write(maxShortBytes)
} else {
numEntries := v.Len()
for i := 0; i < numEntries; i++ {
if i > 0 {
f.fs.Write(spaceBytes)
}
f.ignoreNextType = true
f.format(f.unpackValue(v.Index(i)))
}
}
f.depth--
f.fs.Write(closeBracketBytes)
case reflect.String:
f.fs.Write([]byte(v.String()))
case reflect.Interface:
// The only time we should get here is for nil interfaces due to
// unpackValue calls.
if v.IsNil() {
f.fs.Write(nilAngleBytes)
}
case reflect.Ptr:
// Do nothing. We should never get here since pointers have already
// been handled above.
case reflect.Map:
// nil maps should be indicated as different than empty maps
if v.IsNil() {
f.fs.Write(nilAngleBytes)
break
}
f.fs.Write(openMapBytes)
f.depth++
if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
f.fs.Write(maxShortBytes)
} else {
keys := v.MapKeys()
if f.cs.SortKeys {
sortValues(keys, f.cs)
}
for i, key := range keys {
if i > 0 {
f.fs.Write(spaceBytes)
}
f.ignoreNextType = true
f.format(f.unpackValue(key))
f.fs.Write(colonBytes)
f.ignoreNextType = true
f.format(f.unpackValue(v.MapIndex(key)))
}
}
f.depth--
f.fs.Write(closeMapBytes)
case reflect.Struct:
numFields := v.NumField()
f.fs.Write(openBraceBytes)
f.depth++
if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
f.fs.Write(maxShortBytes)
} else {
vt := v.Type()
for i := 0; i < numFields; i++ {
if i > 0 {
f.fs.Write(spaceBytes)
}
vtf := vt.Field(i)
if f.fs.Flag('+') || f.fs.Flag('#') {
f.fs.Write([]byte(vtf.Name))
f.fs.Write(colonBytes)
}
f.format(f.unpackValue(v.Field(i)))
}
}
f.depth--
f.fs.Write(closeBraceBytes)
case reflect.Uintptr:
printHexPtr(f.fs, uintptr(v.Uint()))
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
printHexPtr(f.fs, v.Pointer())
// There were not any other types at the time this code was written, but
// fall back to letting the default fmt package handle it if any get added.
default:
format := f.buildDefaultFormat()
if v.CanInterface() {
fmt.Fprintf(f.fs, format, v.Interface())
} else {
fmt.Fprintf(f.fs, format, v.String())
}
}
}
// Format satisfies the fmt.Formatter interface. See NewFormatter for usage
// details.
func (f *formatState) Format(fs fmt.State, verb rune) {
f.fs = fs
// Use standard formatting for verbs that are not v.
if verb != 'v' {
format := f.constructOrigFormat(verb)
fmt.Fprintf(fs, format, f.value)
return
}
if f.value == nil {
if fs.Flag('#') {
fs.Write(interfaceBytes)
}
fs.Write(nilAngleBytes)
return
}
f.format(reflect.ValueOf(f.value))
}
// newFormatter is a helper function to consolidate the logic from the various
// public methods which take varying config states.
func newFormatter(cs *ConfigState, v interface{}) fmt.Formatter {
fs := &formatState{value: v, cs: cs}
fs.pointers = make(map[uintptr]int)
return fs
}
/*
NewFormatter returns a custom formatter that satisfies the fmt.Formatter
interface. As a result, it integrates cleanly with standard fmt package
printing functions. The formatter is useful for inline printing of smaller data
types similar to the standard %v format specifier.
The custom formatter only responds to the %v (most compact), %+v (adds pointer
addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb
combinations. Any other verbs such as %x and %q will be sent to the the
standard fmt package for formatting. In addition, the custom formatter ignores
the width and precision arguments (however they will still work on the format
specifiers not handled by the custom formatter).
Typically this function shouldn't be called directly. It is much easier to make
use of the custom formatter by calling one of the convenience functions such as
Printf, Println, or Fprintf.
*/
func NewFormatter(v interface{}) fmt.Formatter {
return newFormatter(&Config, v)
}

148
vendor/github.com/davecgh/go-spew/spew/spew.go generated vendored Normal file
View File

@ -0,0 +1,148 @@
/*
* 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
* 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
import (
"fmt"
"io"
)
// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were
// passed with a default Formatter interface returned by NewFormatter. It
// returns the formatted string as a value that satisfies error. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Errorf(format, spew.NewFormatter(a), spew.NewFormatter(b))
func Errorf(format string, a ...interface{}) (err error) {
return fmt.Errorf(format, convertArgs(a)...)
}
// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were
// passed with a default Formatter interface returned by NewFormatter. It
// returns the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Fprint(w, spew.NewFormatter(a), spew.NewFormatter(b))
func Fprint(w io.Writer, a ...interface{}) (n int, err error) {
return fmt.Fprint(w, convertArgs(a)...)
}
// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were
// passed with a default Formatter interface returned by NewFormatter. It
// returns the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Fprintf(w, format, spew.NewFormatter(a), spew.NewFormatter(b))
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
return fmt.Fprintf(w, format, convertArgs(a)...)
}
// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it
// passed with a default Formatter interface returned by NewFormatter. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Fprintln(w, spew.NewFormatter(a), spew.NewFormatter(b))
func Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
return fmt.Fprintln(w, convertArgs(a)...)
}
// Print is a wrapper for fmt.Print that treats each argument as if it were
// passed with a default Formatter interface returned by NewFormatter. It
// returns the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Print(spew.NewFormatter(a), spew.NewFormatter(b))
func Print(a ...interface{}) (n int, err error) {
return fmt.Print(convertArgs(a)...)
}
// Printf is a wrapper for fmt.Printf that treats each argument as if it were
// passed with a default Formatter interface returned by NewFormatter. It
// returns the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Printf(format, spew.NewFormatter(a), spew.NewFormatter(b))
func Printf(format string, a ...interface{}) (n int, err error) {
return fmt.Printf(format, convertArgs(a)...)
}
// Println is a wrapper for fmt.Println that treats each argument as if it were
// passed with a default Formatter interface returned by NewFormatter. It
// returns the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Println(spew.NewFormatter(a), spew.NewFormatter(b))
func Println(a ...interface{}) (n int, err error) {
return fmt.Println(convertArgs(a)...)
}
// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were
// passed with a default Formatter interface returned by NewFormatter. It
// returns the resulting string. See NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Sprint(spew.NewFormatter(a), spew.NewFormatter(b))
func Sprint(a ...interface{}) string {
return fmt.Sprint(convertArgs(a)...)
}
// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were
// passed with a default Formatter interface returned by NewFormatter. It
// returns the resulting string. See NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Sprintf(format, spew.NewFormatter(a), spew.NewFormatter(b))
func Sprintf(format string, a ...interface{}) string {
return fmt.Sprintf(format, convertArgs(a)...)
}
// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it
// were passed with a default Formatter interface returned by NewFormatter. It
// returns the resulting string. See NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Sprintln(spew.NewFormatter(a), spew.NewFormatter(b))
func Sprintln(a ...interface{}) string {
return fmt.Sprintln(convertArgs(a)...)
}
// convertArgs accepts a slice of arguments and returns a slice of the same
// length with each argument converted to a default spew Formatter interface.
func convertArgs(args []interface{}) (formatters []interface{}) {
formatters = make([]interface{}, len(args))
for index, arg := range args {
formatters[index] = NewFormatter(arg)
}
return formatters
}

4
vendor/github.com/dgrijalva/jwt-go/.gitignore generated vendored Normal file
View File

@ -0,0 +1,4 @@
.DS_Store
bin

13
vendor/github.com/dgrijalva/jwt-go/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,13 @@
language: go
script:
- go vet ./...
- go test -v ./...
go:
- 1.3
- 1.4
- 1.5
- 1.6
- 1.7
- tip

View File

@ -1,54 +0,0 @@
// Copyright 2014 Gary Burd
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package internal // import "github.com/garyburd/redigo/internal"
import (
"strings"
)
const (
WatchState = 1 << iota
MultiState
SubscribeState
MonitorState
)
type CommandInfo struct {
Set, Clear int
}
var commandInfos = map[string]CommandInfo{
"WATCH": {Set: WatchState},
"UNWATCH": {Clear: WatchState},
"MULTI": {Set: MultiState},
"EXEC": {Clear: WatchState | MultiState},
"DISCARD": {Clear: WatchState | MultiState},
"PSUBSCRIBE": {Set: SubscribeState},
"SUBSCRIBE": {Set: SubscribeState},
"MONITOR": {Set: MonitorState},
}
func init() {
for n, ci := range commandInfos {
commandInfos[strings.ToLower(n)] = ci
}
}
func LookupCommandInfo(commandName string) CommandInfo {
if ci, ok := commandInfos[commandName]; ok {
return ci
}
return commandInfos[strings.ToUpper(commandName)]
}

View File

@ -1,651 +0,0 @@
// Copyright 2012 Gary Burd
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package redis
import (
"bufio"
"bytes"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"net/url"
"regexp"
"strconv"
"sync"
"time"
)
// conn is the low-level implementation of Conn
type conn struct {
// Shared
mu sync.Mutex
pending int
err error
conn net.Conn
// Read
readTimeout time.Duration
br *bufio.Reader
// Write
writeTimeout time.Duration
bw *bufio.Writer
// Scratch space for formatting argument length.
// '*' or '$', length, "\r\n"
lenScratch [32]byte
// Scratch space for formatting integers and floats.
numScratch [40]byte
}
// DialTimeout acts like Dial but takes timeouts for establishing the
// connection to the server, writing a command and reading a reply.
//
// Deprecated: Use Dial with options instead.
func DialTimeout(network, address string, connectTimeout, readTimeout, writeTimeout time.Duration) (Conn, error) {
return Dial(network, address,
DialConnectTimeout(connectTimeout),
DialReadTimeout(readTimeout),
DialWriteTimeout(writeTimeout))
}
// DialOption specifies an option for dialing a Redis server.
type DialOption struct {
f func(*dialOptions)
}
type dialOptions struct {
readTimeout time.Duration
writeTimeout time.Duration
dialer *net.Dialer
dial func(network, addr string) (net.Conn, error)
db int
password string
useTLS bool
skipVerify bool
tlsConfig *tls.Config
}
// DialReadTimeout specifies the timeout for reading a single command reply.
func DialReadTimeout(d time.Duration) DialOption {
return DialOption{func(do *dialOptions) {
do.readTimeout = d
}}
}
// DialWriteTimeout specifies the timeout for writing a single command.
func DialWriteTimeout(d time.Duration) DialOption {
return DialOption{func(do *dialOptions) {
do.writeTimeout = d
}}
}
// DialConnectTimeout specifies the timeout for connecting to the Redis server when
// no DialNetDial option is specified.
func DialConnectTimeout(d time.Duration) DialOption {
return DialOption{func(do *dialOptions) {
do.dialer.Timeout = d
}}
}
// DialKeepAlive specifies the keep-alive period for TCP connections to the Redis server
// when no DialNetDial option is specified.
// If zero, keep-alives are not enabled. If no DialKeepAlive option is specified then
// the default of 5 minutes is used to ensure that half-closed TCP sessions are detected.
func DialKeepAlive(d time.Duration) DialOption {
return DialOption{func(do *dialOptions) {
do.dialer.KeepAlive = d
}}
}
// DialNetDial specifies a custom dial function for creating TCP
// connections, otherwise a net.Dialer customized via the other options is used.
// DialNetDial overrides DialConnectTimeout and DialKeepAlive.
func DialNetDial(dial func(network, addr string) (net.Conn, error)) DialOption {
return DialOption{func(do *dialOptions) {
do.dial = dial
}}
}
// DialDatabase specifies the database to select when dialing a connection.
func DialDatabase(db int) DialOption {
return DialOption{func(do *dialOptions) {
do.db = db
}}
}
// DialPassword specifies the password to use when connecting to
// the Redis server.
func DialPassword(password string) DialOption {
return DialOption{func(do *dialOptions) {
do.password = password
}}
}
// DialTLSConfig specifies the config to use when a TLS connection is dialed.
// Has no effect when not dialing a TLS connection.
func DialTLSConfig(c *tls.Config) DialOption {
return DialOption{func(do *dialOptions) {
do.tlsConfig = c
}}
}
// DialTLSSkipVerify disables server name verification when connecting over
// TLS. Has no effect when not dialing a TLS connection.
func DialTLSSkipVerify(skip bool) DialOption {
return DialOption{func(do *dialOptions) {
do.skipVerify = skip
}}
}
// DialUseTLS specifies whether TLS should be used when connecting to the
// server. This option is ignore by DialURL.
func DialUseTLS(useTLS bool) DialOption {
return DialOption{func(do *dialOptions) {
do.useTLS = useTLS
}}
}
// Dial connects to the Redis server at the given network and
// address using the specified options.
func Dial(network, address string, options ...DialOption) (Conn, error) {
do := dialOptions{
dialer: &net.Dialer{
KeepAlive: time.Minute * 5,
},
}
for _, option := range options {
option.f(&do)
}
if do.dial == nil {
do.dial = do.dialer.Dial
}
netConn, err := do.dial(network, address)
if err != nil {
return nil, err
}
if do.useTLS {
tlsConfig := cloneTLSClientConfig(do.tlsConfig, do.skipVerify)
if tlsConfig.ServerName == "" {
host, _, err := net.SplitHostPort(address)
if err != nil {
netConn.Close()
return nil, err
}
tlsConfig.ServerName = host
}
tlsConn := tls.Client(netConn, tlsConfig)
if err := tlsConn.Handshake(); err != nil {
netConn.Close()
return nil, err
}
netConn = tlsConn
}
c := &conn{
conn: netConn,
bw: bufio.NewWriter(netConn),
br: bufio.NewReader(netConn),
readTimeout: do.readTimeout,
writeTimeout: do.writeTimeout,
}
if do.password != "" {
if _, err := c.Do("AUTH", do.password); err != nil {
netConn.Close()
return nil, err
}
}
if do.db != 0 {
if _, err := c.Do("SELECT", do.db); err != nil {
netConn.Close()
return nil, err
}
}
return c, nil
}
var pathDBRegexp = regexp.MustCompile(`/(\d*)\z`)
// DialURL connects to a Redis server at the given URL using the Redis
// URI scheme. URLs should follow the draft IANA specification for the
// scheme (https://www.iana.org/assignments/uri-schemes/prov/redis).
func DialURL(rawurl string, options ...DialOption) (Conn, error) {
u, err := url.Parse(rawurl)
if err != nil {
return nil, err
}
if u.Scheme != "redis" && u.Scheme != "rediss" {
return nil, fmt.Errorf("invalid redis URL scheme: %s", u.Scheme)
}
// As per the IANA draft spec, the host defaults to localhost and
// the port defaults to 6379.
host, port, err := net.SplitHostPort(u.Host)
if err != nil {
// assume port is missing
host = u.Host
port = "6379"
}
if host == "" {
host = "localhost"
}
address := net.JoinHostPort(host, port)
if u.User != nil {
password, isSet := u.User.Password()
if isSet {
options = append(options, DialPassword(password))
}
}
match := pathDBRegexp.FindStringSubmatch(u.Path)
if len(match) == 2 {
db := 0
if len(match[1]) > 0 {
db, err = strconv.Atoi(match[1])
if err != nil {
return nil, fmt.Errorf("invalid database: %s", u.Path[1:])
}
}
if db != 0 {
options = append(options, DialDatabase(db))
}
} else if u.Path != "" {
return nil, fmt.Errorf("invalid database: %s", u.Path[1:])
}
options = append(options, DialUseTLS(u.Scheme == "rediss"))
return Dial("tcp", address, options...)
}
// NewConn returns a new Redigo connection for the given net connection.
func NewConn(netConn net.Conn, readTimeout, writeTimeout time.Duration) Conn {
return &conn{
conn: netConn,
bw: bufio.NewWriter(netConn),
br: bufio.NewReader(netConn),
readTimeout: readTimeout,
writeTimeout: writeTimeout,
}
}
func (c *conn) Close() error {
c.mu.Lock()
err := c.err
if c.err == nil {
c.err = errors.New("redigo: closed")
err = c.conn.Close()
}
c.mu.Unlock()
return err
}
func (c *conn) fatal(err error) error {
c.mu.Lock()
if c.err == nil {
c.err = err
// Close connection to force errors on subsequent calls and to unblock
// other reader or writer.
c.conn.Close()
}
c.mu.Unlock()
return err
}
func (c *conn) Err() error {
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
}
func (c *conn) writeLen(prefix byte, n int) error {
c.lenScratch[len(c.lenScratch)-1] = '\n'
c.lenScratch[len(c.lenScratch)-2] = '\r'
i := len(c.lenScratch) - 3
for {
c.lenScratch[i] = byte('0' + n%10)
i -= 1
n = n / 10
if n == 0 {
break
}
}
c.lenScratch[i] = prefix
_, err := c.bw.Write(c.lenScratch[i:])
return err
}
func (c *conn) writeString(s string) error {
c.writeLen('$', len(s))
c.bw.WriteString(s)
_, err := c.bw.WriteString("\r\n")
return err
}
func (c *conn) writeBytes(p []byte) error {
c.writeLen('$', len(p))
c.bw.Write(p)
_, err := c.bw.WriteString("\r\n")
return err
}
func (c *conn) writeInt64(n int64) error {
return c.writeBytes(strconv.AppendInt(c.numScratch[:0], n, 10))
}
func (c *conn) writeFloat64(n float64) error {
return c.writeBytes(strconv.AppendFloat(c.numScratch[:0], n, 'g', -1, 64))
}
func (c *conn) writeCommand(cmd string, args []interface{}) error {
c.writeLen('*', 1+len(args))
if err := c.writeString(cmd); err != nil {
return err
}
for _, arg := range args {
if err := c.writeArg(arg, true); err != nil {
return err
}
}
return nil
}
func (c *conn) writeArg(arg interface{}, argumentTypeOK bool) (err error) {
switch arg := arg.(type) {
case string:
return c.writeString(arg)
case []byte:
return c.writeBytes(arg)
case int:
return c.writeInt64(int64(arg))
case int64:
return c.writeInt64(arg)
case float64:
return c.writeFloat64(arg)
case bool:
if arg {
return c.writeString("1")
} else {
return c.writeString("0")
}
case nil:
return c.writeString("")
case Argument:
if argumentTypeOK {
return c.writeArg(arg.RedisArg(), false)
}
// See comment in default clause below.
var buf bytes.Buffer
fmt.Fprint(&buf, arg)
return c.writeBytes(buf.Bytes())
default:
// This default clause is intended to handle builtin numeric types.
// The function should return an error for other types, but this is not
// done for compatibility with previous versions of the package.
var buf bytes.Buffer
fmt.Fprint(&buf, arg)
return c.writeBytes(buf.Bytes())
}
}
type protocolError string
func (pe protocolError) Error() string {
return fmt.Sprintf("redigo: %s (possible server error or unsupported concurrent read by application)", string(pe))
}
func (c *conn) readLine() ([]byte, error) {
p, err := c.br.ReadSlice('\n')
if err == bufio.ErrBufferFull {
return nil, protocolError("long response line")
}
if err != nil {
return nil, err
}
i := len(p) - 2
if i < 0 || p[i] != '\r' {
return nil, protocolError("bad response line terminator")
}
return p[:i], nil
}
// parseLen parses bulk string and array lengths.
func parseLen(p []byte) (int, error) {
if len(p) == 0 {
return -1, protocolError("malformed length")
}
if p[0] == '-' && len(p) == 2 && p[1] == '1' {
// handle $-1 and $-1 null replies.
return -1, nil
}
var n int
for _, b := range p {
n *= 10
if b < '0' || b > '9' {
return -1, protocolError("illegal bytes in length")
}
n += int(b - '0')
}
return n, nil
}
// parseInt parses an integer reply.
func parseInt(p []byte) (interface{}, error) {
if len(p) == 0 {
return 0, protocolError("malformed integer")
}
var negate bool
if p[0] == '-' {
negate = true
p = p[1:]
if len(p) == 0 {
return 0, protocolError("malformed integer")
}
}
var n int64
for _, b := range p {
n *= 10
if b < '0' || b > '9' {
return 0, protocolError("illegal bytes in length")
}
n += int64(b - '0')
}
if negate {
n = -n
}
return n, nil
}
var (
okReply interface{} = "OK"
pongReply interface{} = "PONG"
)
func (c *conn) readReply() (interface{}, error) {
line, err := c.readLine()
if err != nil {
return nil, err
}
if len(line) == 0 {
return nil, protocolError("short response line")
}
switch line[0] {
case '+':
switch {
case len(line) == 3 && line[1] == 'O' && line[2] == 'K':
// Avoid allocation for frequent "+OK" response.
return okReply, nil
case len(line) == 5 && line[1] == 'P' && line[2] == 'O' && line[3] == 'N' && line[4] == 'G':
// Avoid allocation in PING command benchmarks :)
return pongReply, nil
default:
return string(line[1:]), nil
}
case '-':
return Error(string(line[1:])), nil
case ':':
return parseInt(line[1:])
case '$':
n, err := parseLen(line[1:])
if n < 0 || err != nil {
return nil, err
}
p := make([]byte, n)
_, err = io.ReadFull(c.br, p)
if err != nil {
return nil, err
}
if line, err := c.readLine(); err != nil {
return nil, err
} else if len(line) != 0 {
return nil, protocolError("bad bulk string format")
}
return p, nil
case '*':
n, err := parseLen(line[1:])
if n < 0 || err != nil {
return nil, err
}
r := make([]interface{}, n)
for i := range r {
r[i], err = c.readReply()
if err != nil {
return nil, err
}
}
return r, nil
}
return nil, protocolError("unexpected response line")
}
func (c *conn) Send(cmd string, args ...interface{}) error {
c.mu.Lock()
c.pending += 1
c.mu.Unlock()
if c.writeTimeout != 0 {
c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
}
if err := c.writeCommand(cmd, args); err != nil {
return c.fatal(err)
}
return nil
}
func (c *conn) Flush() error {
if c.writeTimeout != 0 {
c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
}
if err := c.bw.Flush(); err != nil {
return c.fatal(err)
}
return nil
}
func (c *conn) Receive() (reply interface{}, err error) {
if c.readTimeout != 0 {
c.conn.SetReadDeadline(time.Now().Add(c.readTimeout))
}
if reply, err = c.readReply(); err != nil {
return nil, c.fatal(err)
}
// When using pub/sub, the number of receives can be greater than the
// number of sends. To enable normal use of the connection after
// unsubscribing from all channels, we do not decrement pending to a
// negative value.
//
// The pending field is decremented after the reply is read to handle the
// case where Receive is called before Send.
c.mu.Lock()
if c.pending > 0 {
c.pending -= 1
}
c.mu.Unlock()
if err, ok := reply.(Error); ok {
return nil, err
}
return
}
func (c *conn) Do(cmd string, args ...interface{}) (interface{}, error) {
c.mu.Lock()
pending := c.pending
c.pending = 0
c.mu.Unlock()
if cmd == "" && pending == 0 {
return nil, nil
}
if c.writeTimeout != 0 {
c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
}
if cmd != "" {
if err := c.writeCommand(cmd, args); err != nil {
return nil, c.fatal(err)
}
}
if err := c.bw.Flush(); err != nil {
return nil, c.fatal(err)
}
if c.readTimeout != 0 {
c.conn.SetReadDeadline(time.Now().Add(c.readTimeout))
}
if cmd == "" {
reply := make([]interface{}, pending)
for i := range reply {
r, e := c.readReply()
if e != nil {
return nil, c.fatal(e)
}
reply[i] = r
}
return reply, nil
}
var err error
var reply interface{}
for i := 0; i <= pending; i++ {
var e error
if reply, e = c.readReply(); e != nil {
return nil, c.fatal(e)
}
if e, ok := reply.(Error); ok && err == nil {
err = e
}
}
return reply, err
}

View File

@ -1,177 +0,0 @@
// Copyright 2012 Gary Burd
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
// Package redis is a client for the Redis database.
//
// The Redigo FAQ (https://github.com/garyburd/redigo/wiki/FAQ) contains more
// documentation about this package.
//
// Connections
//
// The Conn interface is the primary interface for working with Redis.
// Applications create connections by calling the Dial, DialWithTimeout or
// NewConn functions. In the future, functions will be added for creating
// sharded and other types of connections.
//
// The application must call the connection Close method when the application
// is done with the connection.
//
// Executing Commands
//
// The Conn interface has a generic method for executing Redis commands:
//
// Do(commandName string, args ...interface{}) (reply interface{}, err error)
//
// The Redis command reference (http://redis.io/commands) lists the available
// commands. An example of using the Redis APPEND command is:
//
// n, err := conn.Do("APPEND", "key", "value")
//
// The Do method converts command arguments to bulk strings for transmission
// to the server as follows:
//
// Go Type Conversion
// []byte Sent as is
// string Sent as is
// int, int64 strconv.FormatInt(v)
// float64 strconv.FormatFloat(v, 'g', -1, 64)
// bool true -> "1", false -> "0"
// nil ""
// all other types fmt.Fprint(w, v)
//
// Redis command reply types are represented using the following Go types:
//
// Redis type Go type
// error redis.Error
// integer int64
// simple string string
// bulk string []byte or nil if value not present.
// array []interface{} or nil if value not present.
//
// Use type assertions or the reply helper functions to convert from
// interface{} to the specific Go type for the command result.
//
// Pipelining
//
// Connections support pipelining using the Send, Flush and Receive methods.
//
// Send(commandName string, args ...interface{}) error
// Flush() error
// Receive() (reply interface{}, err error)
//
// Send writes the command to the connection's output buffer. Flush flushes the
// connection's output buffer to the server. Receive reads a single reply from
// the server. The following example shows a simple pipeline.
//
// c.Send("SET", "foo", "bar")
// c.Send("GET", "foo")
// c.Flush()
// c.Receive() // reply from SET
// v, err = c.Receive() // reply from GET
//
// The Do method combines the functionality of the Send, Flush and Receive
// methods. The Do method starts by writing the command and flushing the output
// buffer. Next, the Do method receives all pending replies including the reply
// for the command just sent by Do. If any of the received replies is an error,
// then Do returns the error. If there are no errors, then Do returns the last
// reply. If the command argument to the Do method is "", then the Do method
// will flush the output buffer and receive pending replies without sending a
// command.
//
// Use the Send and Do methods to implement pipelined transactions.
//
// c.Send("MULTI")
// c.Send("INCR", "foo")
// c.Send("INCR", "bar")
// r, err := c.Do("EXEC")
// fmt.Println(r) // prints [1, 1]
//
// Concurrency
//
// Connections support one concurrent caller to the Receive method and one
// concurrent caller to the Send and Flush methods. No other concurrency is
// supported including concurrent calls to the Do method.
//
// For full concurrent access to Redis, use the thread-safe Pool to get, use
// and release a connection from within a goroutine. Connections returned from
// a Pool have the concurrency restrictions described in the previous
// paragraph.
//
// Publish and Subscribe
//
// Use the Send, Flush and Receive methods to implement Pub/Sub subscribers.
//
// c.Send("SUBSCRIBE", "example")
// c.Flush()
// for {
// reply, err := c.Receive()
// if err != nil {
// return err
// }
// // process pushed message
// }
//
// The PubSubConn type wraps a Conn with convenience methods for implementing
// subscribers. The Subscribe, PSubscribe, Unsubscribe and PUnsubscribe methods
// send and flush a subscription management command. The receive method
// converts a pushed message to convenient types for use in a type switch.
//
// psc := redis.PubSubConn{Conn: c}
// psc.Subscribe("example")
// for {
// switch v := psc.Receive().(type) {
// case redis.Message:
// fmt.Printf("%s: message: %s\n", v.Channel, v.Data)
// case redis.Subscription:
// fmt.Printf("%s: %s %d\n", v.Channel, v.Kind, v.Count)
// case error:
// return v
// }
// }
//
// Reply Helpers
//
// The Bool, Int, Bytes, String, Strings and Values functions convert a reply
// to a value of a specific type. To allow convenient wrapping of calls to the
// connection Do and Receive methods, the functions take a second argument of
// type error. If the error is non-nil, then the helper function returns the
// error. If the error is nil, the function converts the reply to the specified
// type:
//
// exists, err := redis.Bool(c.Do("EXISTS", "foo"))
// if err != nil {
// // handle error return from c.Do or type conversion error.
// }
//
// The Scan function converts elements of a array reply to Go types:
//
// var value1 int
// var value2 string
// reply, err := redis.Values(c.Do("MGET", "key1", "key2"))
// if err != nil {
// // handle error
// }
// if _, err := redis.Scan(reply, &value1, &value2); err != nil {
// // handle error
// }
//
// Errors
//
// Connection methods return error replies from the server as type redis.Error.
//
// Call the connection Err() method to determine if the connection encountered
// non-recoverable error such as a network error or protocol parsing error. If
// Err() returns a non-nil value, then the connection is not usable and should
// be closed.
package redis // import "github.com/garyburd/redigo/redis"

View File

@ -1,33 +0,0 @@
// +build go1.7
package redis
import "crypto/tls"
// similar cloneTLSClientConfig in the stdlib, but also honor skipVerify for the nil case
func cloneTLSClientConfig(cfg *tls.Config, skipVerify bool) *tls.Config {
if cfg == nil {
return &tls.Config{InsecureSkipVerify: skipVerify}
}
return &tls.Config{
Rand: cfg.Rand,
Time: cfg.Time,
Certificates: cfg.Certificates,
NameToCertificate: cfg.NameToCertificate,
GetCertificate: cfg.GetCertificate,
RootCAs: cfg.RootCAs,
NextProtos: cfg.NextProtos,
ServerName: cfg.ServerName,
ClientAuth: cfg.ClientAuth,
ClientCAs: cfg.ClientCAs,
InsecureSkipVerify: cfg.InsecureSkipVerify,
CipherSuites: cfg.CipherSuites,
PreferServerCipherSuites: cfg.PreferServerCipherSuites,
ClientSessionCache: cfg.ClientSessionCache,
MinVersion: cfg.MinVersion,
MaxVersion: cfg.MaxVersion,
CurvePreferences: cfg.CurvePreferences,
DynamicRecordSizingDisabled: cfg.DynamicRecordSizingDisabled,
Renegotiation: cfg.Renegotiation,
}
}

View File

@ -1,117 +0,0 @@
// Copyright 2012 Gary Burd
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package redis
import (
"bytes"
"fmt"
"log"
)
// NewLoggingConn returns a logging wrapper around a connection.
func NewLoggingConn(conn Conn, logger *log.Logger, prefix string) Conn {
if prefix != "" {
prefix = prefix + "."
}
return &loggingConn{conn, logger, prefix}
}
type loggingConn struct {
Conn
logger *log.Logger
prefix string
}
func (c *loggingConn) Close() error {
err := c.Conn.Close()
var buf bytes.Buffer
fmt.Fprintf(&buf, "%sClose() -> (%v)", c.prefix, err)
c.logger.Output(2, buf.String())
return err
}
func (c *loggingConn) printValue(buf *bytes.Buffer, v interface{}) {
const chop = 32
switch v := v.(type) {
case []byte:
if len(v) > chop {
fmt.Fprintf(buf, "%q...", v[:chop])
} else {
fmt.Fprintf(buf, "%q", v)
}
case string:
if len(v) > chop {
fmt.Fprintf(buf, "%q...", v[:chop])
} else {
fmt.Fprintf(buf, "%q", v)
}
case []interface{}:
if len(v) == 0 {
buf.WriteString("[]")
} else {
sep := "["
fin := "]"
if len(v) > chop {
v = v[:chop]
fin = "...]"
}
for _, vv := range v {
buf.WriteString(sep)
c.printValue(buf, vv)
sep = ", "
}
buf.WriteString(fin)
}
default:
fmt.Fprint(buf, v)
}
}
func (c *loggingConn) print(method, commandName string, args []interface{}, reply interface{}, err error) {
var buf bytes.Buffer
fmt.Fprintf(&buf, "%s%s(", c.prefix, method)
if method != "Receive" {
buf.WriteString(commandName)
for _, arg := range args {
buf.WriteString(", ")
c.printValue(&buf, arg)
}
}
buf.WriteString(") -> (")
if method != "Send" {
c.printValue(&buf, reply)
buf.WriteString(", ")
}
fmt.Fprintf(&buf, "%v)", err)
c.logger.Output(3, buf.String())
}
func (c *loggingConn) Do(commandName string, args ...interface{}) (interface{}, error) {
reply, err := c.Conn.Do(commandName, args...)
c.print("Do", commandName, args, reply, err)
return reply, err
}
func (c *loggingConn) Send(commandName string, args ...interface{}) error {
err := c.Conn.Send(commandName, args...)
c.print("Send", commandName, args, nil, err)
return err
}
func (c *loggingConn) Receive() (interface{}, error) {
reply, err := c.Conn.Receive()
c.print("Receive", "", nil, reply, err)
return reply, err
}

View File

@ -1,442 +0,0 @@
// Copyright 2012 Gary Burd
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package redis
import (
"bytes"
"container/list"
"crypto/rand"
"crypto/sha1"
"errors"
"io"
"strconv"
"sync"
"time"
"github.com/garyburd/redigo/internal"
)
var nowFunc = time.Now // for testing
// ErrPoolExhausted is returned from a pool connection method (Do, Send,
// Receive, Flush, Err) when the maximum number of database connections in the
// pool has been reached.
var ErrPoolExhausted = errors.New("redigo: connection pool exhausted")
var (
errPoolClosed = errors.New("redigo: connection pool closed")
errConnClosed = errors.New("redigo: connection closed")
)
// Pool maintains a pool of connections. The application calls the Get method
// to get a connection from the pool and the connection's Close method to
// return the connection's resources to the pool.
//
// The following example shows how to use a pool in a web application. The
// application creates a pool at application startup and makes it available to
// request handlers using a package level variable. The pool configuration used
// here is an example, not a recommendation.
//
// func newPool(addr string) *redis.Pool {
// return &redis.Pool{
// MaxIdle: 3,
// IdleTimeout: 240 * time.Second,
// Dial: func () (redis.Conn, error) { return redis.Dial("tcp", addr) },
// }
// }
//
// var (
// pool *redis.Pool
// redisServer = flag.String("redisServer", ":6379", "")
// )
//
// func main() {
// flag.Parse()
// pool = newPool(*redisServer)
// ...
// }
//
// A request handler gets a connection from the pool and closes the connection
// when the handler is done:
//
// func serveHome(w http.ResponseWriter, r *http.Request) {
// conn := pool.Get()
// defer conn.Close()
// ...
// }
//
// Use the Dial function to authenticate connections with the AUTH command or
// select a database with the SELECT command:
//
// pool := &redis.Pool{
// // Other pool configuration not shown in this example.
// Dial: func () (redis.Conn, error) {
// c, err := redis.Dial("tcp", server)
// if err != nil {
// return nil, err
// }
// if _, err := c.Do("AUTH", password); err != nil {
// c.Close()
// return nil, err
// }
// if _, err := c.Do("SELECT", db); err != nil {
// c.Close()
// return nil, err
// }
// return c, nil
// }
// }
//
// Use the TestOnBorrow function to check the health of an idle connection
// before the connection is returned to the application. This example PINGs
// connections that have been idle more than a minute:
//
// pool := &redis.Pool{
// // Other pool configuration not shown in this example.
// TestOnBorrow: func(c redis.Conn, t time.Time) error {
// if time.Since(t) < time.Minute {
// return nil
// }
// _, err := c.Do("PING")
// return err
// },
// }
//
type Pool struct {
// Dial is an application supplied function for creating and configuring a
// connection.
//
// The connection returned from Dial must not be in a special state
// (subscribed to pubsub channel, transaction started, ...).
Dial func() (Conn, error)
// TestOnBorrow is an optional application supplied function for checking
// the health of an idle connection before the connection is used again by
// the application. Argument t is the time that the connection was returned
// to the pool. If the function returns an error, then the connection is
// closed.
TestOnBorrow func(c Conn, t time.Time) error
// Maximum number of idle connections in the pool.
MaxIdle int
// Maximum number of connections allocated by the pool at a given time.
// When zero, there is no limit on the number of connections in the pool.
MaxActive int
// Close connections after remaining idle for this duration. If the value
// is zero, then idle connections are not closed. Applications should set
// the timeout to a value less than the server's timeout.
IdleTimeout time.Duration
// If Wait is true and the pool is at the MaxActive limit, then Get() waits
// for a connection to be returned to the pool before returning.
Wait bool
// mu protects fields defined below.
mu sync.Mutex
cond *sync.Cond
closed bool
active int
// Stack of idleConn with most recently used at the front.
idle list.List
}
type idleConn struct {
c Conn
t time.Time
}
// NewPool creates a new pool.
//
// Deprecated: Initialize the Pool directory as shown in the example.
func NewPool(newFn func() (Conn, error), maxIdle int) *Pool {
return &Pool{Dial: newFn, MaxIdle: maxIdle}
}
// Get gets a connection. The application must close the returned connection.
// This method always returns a valid connection so that applications can defer
// error handling to the first use of the connection. If there is an error
// getting an underlying connection, then the connection Err, Do, Send, Flush
// and Receive methods return that error.
func (p *Pool) Get() Conn {
c, err := p.get()
if err != nil {
return errorConnection{err}
}
return &pooledConnection{p: p, c: c}
}
// PoolStats contains pool statistics.
type PoolStats struct {
// ActiveCount is the number of connections in the pool. The count includes idle connections and connections in use.
ActiveCount int
// IdleCount is the number of idle connections in the pool.
IdleCount int
}
// Stats returns pool's statistics.
func (p *Pool) Stats() PoolStats {
p.mu.Lock()
stats := PoolStats{
ActiveCount: p.active,
IdleCount: p.idle.Len(),
}
p.mu.Unlock()
return stats
}
// ActiveCount returns the number of connections in the pool. The count includes idle connections and connections in use.
func (p *Pool) ActiveCount() int {
p.mu.Lock()
active := p.active
p.mu.Unlock()
return active
}
// IdleCount returns the number of idle connections in the pool.
func (p *Pool) IdleCount() int {
p.mu.Lock()
idle := p.idle.Len()
p.mu.Unlock()
return idle
}
// Close releases the resources used by the pool.
func (p *Pool) Close() error {
p.mu.Lock()
idle := p.idle
p.idle.Init()
p.closed = true
p.active -= idle.Len()
if p.cond != nil {
p.cond.Broadcast()
}
p.mu.Unlock()
for e := idle.Front(); e != nil; e = e.Next() {
e.Value.(idleConn).c.Close()
}
return nil
}
// release decrements the active count and signals waiters. The caller must
// hold p.mu during the call.
func (p *Pool) release() {
p.active -= 1
if p.cond != nil {
p.cond.Signal()
}
}
// get prunes stale connections and returns a connection from the idle list or
// creates a new connection.
func (p *Pool) get() (Conn, error) {
p.mu.Lock()
// Prune stale connections.
if timeout := p.IdleTimeout; timeout > 0 {
for i, n := 0, p.idle.Len(); i < n; i++ {
e := p.idle.Back()
if e == nil {
break
}
ic := e.Value.(idleConn)
if ic.t.Add(timeout).After(nowFunc()) {
break
}
p.idle.Remove(e)
p.release()
p.mu.Unlock()
ic.c.Close()
p.mu.Lock()
}
}
for {
// Get idle connection.
for i, n := 0, p.idle.Len(); i < n; i++ {
e := p.idle.Front()
if e == nil {
break
}
ic := e.Value.(idleConn)
p.idle.Remove(e)
test := p.TestOnBorrow
p.mu.Unlock()
if test == nil || test(ic.c, ic.t) == nil {
return ic.c, nil
}
ic.c.Close()
p.mu.Lock()
p.release()
}
// Check for pool closed before dialing a new connection.
if p.closed {
p.mu.Unlock()
return nil, errors.New("redigo: get on closed pool")
}
// Dial new connection if under limit.
if p.MaxActive == 0 || p.active < p.MaxActive {
dial := p.Dial
p.active += 1
p.mu.Unlock()
c, err := dial()
if err != nil {
p.mu.Lock()
p.release()
p.mu.Unlock()
c = nil
}
return c, err
}
if !p.Wait {
p.mu.Unlock()
return nil, ErrPoolExhausted
}
if p.cond == nil {
p.cond = sync.NewCond(&p.mu)
}
p.cond.Wait()
}
}
func (p *Pool) put(c Conn, forceClose bool) error {
err := c.Err()
p.mu.Lock()
if !p.closed && err == nil && !forceClose {
p.idle.PushFront(idleConn{t: nowFunc(), c: c})
if p.idle.Len() > p.MaxIdle {
c = p.idle.Remove(p.idle.Back()).(idleConn).c
} else {
c = nil
}
}
if c == nil {
if p.cond != nil {
p.cond.Signal()
}
p.mu.Unlock()
return nil
}
p.release()
p.mu.Unlock()
return c.Close()
}
type pooledConnection struct {
p *Pool
c Conn
state int
}
var (
sentinel []byte
sentinelOnce sync.Once
)
func initSentinel() {
p := make([]byte, 64)
if _, err := rand.Read(p); err == nil {
sentinel = p
} else {
h := sha1.New()
io.WriteString(h, "Oops, rand failed. Use time instead.")
io.WriteString(h, strconv.FormatInt(time.Now().UnixNano(), 10))
sentinel = h.Sum(nil)
}
}
func (pc *pooledConnection) Close() error {
c := pc.c
if _, ok := c.(errorConnection); ok {
return nil
}
pc.c = errorConnection{errConnClosed}
if pc.state&internal.MultiState != 0 {
c.Send("DISCARD")
pc.state &^= (internal.MultiState | internal.WatchState)
} else if pc.state&internal.WatchState != 0 {
c.Send("UNWATCH")
pc.state &^= internal.WatchState
}
if pc.state&internal.SubscribeState != 0 {
c.Send("UNSUBSCRIBE")
c.Send("PUNSUBSCRIBE")
// To detect the end of the message stream, ask the server to echo
// a sentinel value and read until we see that value.
sentinelOnce.Do(initSentinel)
c.Send("ECHO", sentinel)
c.Flush()
for {
p, err := c.Receive()
if err != nil {
break
}
if p, ok := p.([]byte); ok && bytes.Equal(p, sentinel) {
pc.state &^= internal.SubscribeState
break
}
}
}
c.Do("")
pc.p.put(c, pc.state != 0)
return nil
}
func (pc *pooledConnection) Err() error {
return pc.c.Err()
}
func (pc *pooledConnection) Do(commandName string, args ...interface{}) (reply interface{}, err error) {
ci := internal.LookupCommandInfo(commandName)
pc.state = (pc.state | ci.Set) &^ ci.Clear
return pc.c.Do(commandName, args...)
}
func (pc *pooledConnection) Send(commandName string, args ...interface{}) error {
ci := internal.LookupCommandInfo(commandName)
pc.state = (pc.state | ci.Set) &^ ci.Clear
return pc.c.Send(commandName, args...)
}
func (pc *pooledConnection) Flush() error {
return pc.c.Flush()
}
func (pc *pooledConnection) Receive() (reply interface{}, err error) {
return pc.c.Receive()
}
type errorConnection struct{ err error }
func (ec errorConnection) Do(string, ...interface{}) (interface{}, error) { return nil, ec.err }
func (ec errorConnection) Send(string, ...interface{}) error { return ec.err }
func (ec errorConnection) Err() error { return ec.err }
func (ec errorConnection) Close() error { return ec.err }
func (ec errorConnection) Flush() error { return ec.err }
func (ec errorConnection) Receive() (interface{}, error) { return nil, ec.err }

View File

@ -1,31 +0,0 @@
// +build !go1.7
package redis
import "crypto/tls"
// similar cloneTLSClientConfig in the stdlib, but also honor skipVerify for the nil case
func cloneTLSClientConfig(cfg *tls.Config, skipVerify bool) *tls.Config {
if cfg == nil {
return &tls.Config{InsecureSkipVerify: skipVerify}
}
return &tls.Config{
Rand: cfg.Rand,
Time: cfg.Time,
Certificates: cfg.Certificates,
NameToCertificate: cfg.NameToCertificate,
GetCertificate: cfg.GetCertificate,
RootCAs: cfg.RootCAs,
NextProtos: cfg.NextProtos,
ServerName: cfg.ServerName,
ClientAuth: cfg.ClientAuth,
ClientCAs: cfg.ClientCAs,
InsecureSkipVerify: cfg.InsecureSkipVerify,
CipherSuites: cfg.CipherSuites,
PreferServerCipherSuites: cfg.PreferServerCipherSuites,
ClientSessionCache: cfg.ClientSessionCache,
MinVersion: cfg.MinVersion,
MaxVersion: cfg.MaxVersion,
CurvePreferences: cfg.CurvePreferences,
}
}

View File

@ -1,144 +0,0 @@
// Copyright 2012 Gary Burd
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package redis
import "errors"
// Subscription represents a subscribe or unsubscribe notification.
type Subscription struct {
// Kind is "subscribe", "unsubscribe", "psubscribe" or "punsubscribe"
Kind string
// The channel that was changed.
Channel string
// The current number of subscriptions for connection.
Count int
}
// Message represents a message notification.
type Message struct {
// The originating channel.
Channel string
// The message data.
Data []byte
}
// PMessage represents a pmessage notification.
type PMessage struct {
// The matched pattern.
Pattern string
// The originating channel.
Channel string
// The message data.
Data []byte
}
// Pong represents a pubsub pong notification.
type Pong struct {
Data string
}
// PubSubConn wraps a Conn with convenience methods for subscribers.
type PubSubConn struct {
Conn Conn
}
// Close closes the connection.
func (c PubSubConn) Close() error {
return c.Conn.Close()
}
// Subscribe subscribes the connection to the specified channels.
func (c PubSubConn) Subscribe(channel ...interface{}) error {
c.Conn.Send("SUBSCRIBE", channel...)
return c.Conn.Flush()
}
// PSubscribe subscribes the connection to the given patterns.
func (c PubSubConn) PSubscribe(channel ...interface{}) error {
c.Conn.Send("PSUBSCRIBE", channel...)
return c.Conn.Flush()
}
// Unsubscribe unsubscribes the connection from the given channels, or from all
// of them if none is given.
func (c PubSubConn) Unsubscribe(channel ...interface{}) error {
c.Conn.Send("UNSUBSCRIBE", channel...)
return c.Conn.Flush()
}
// PUnsubscribe unsubscribes the connection from the given patterns, or from all
// of them if none is given.
func (c PubSubConn) PUnsubscribe(channel ...interface{}) error {
c.Conn.Send("PUNSUBSCRIBE", channel...)
return c.Conn.Flush()
}
// Ping sends a PING to the server with the specified data.
//
// The connection must be subscribed to at least one channel or pattern when
// calling this method.
func (c PubSubConn) Ping(data string) error {
c.Conn.Send("PING", data)
return c.Conn.Flush()
}
// Receive returns a pushed message as a Subscription, Message, PMessage, Pong
// or error. The return value is intended to be used directly in a type switch
// as illustrated in the PubSubConn example.
func (c PubSubConn) Receive() interface{} {
reply, err := Values(c.Conn.Receive())
if err != nil {
return err
}
var kind string
reply, err = Scan(reply, &kind)
if err != nil {
return err
}
switch kind {
case "message":
var m Message
if _, err := Scan(reply, &m.Channel, &m.Data); err != nil {
return err
}
return m
case "pmessage":
var pm PMessage
if _, err := Scan(reply, &pm.Pattern, &pm.Channel, &pm.Data); err != nil {
return err
}
return pm
case "subscribe", "psubscribe", "unsubscribe", "punsubscribe":
s := Subscription{Kind: kind}
if _, err := Scan(reply, &s.Channel, &s.Count); err != nil {
return err
}
return s
case "pong":
var p Pong
if _, err := Scan(reply, &p.Data); err != nil {
return err
}
return p
}
return errors.New("redigo: unknown pubsub notification")
}

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