Compare commits
113 Commits
Author | SHA1 | Date |
---|---|---|
kolaente | ddc3477efd | |
konrad | 0b1212f428 | |
konrad | c097264bde | |
konrad | 2bc349a33b | |
konrad | 5673acdd60 | |
konrad | 0b4891c3ef | |
kolaente | db5e3a7cbc | |
konrad | 0030372879 | |
konrad | 3de6ecf579 | |
konrad | 91e10eb78b | |
konrad | 5fbc08cf65 | |
konrad | 0763ae07b0 | |
konrad | d553ca743b | |
konrad | 8565772ab0 | |
konrad | cfb68b9f79 | |
konrad | 7a3bc05243 | |
kolaente | 5b7fb1be87 | |
konrad | 0bf37c2ac0 | |
konrad | eebb8eb4c4 | |
konrad | d46cbc2d67 | |
konrad | 537aebc873 | |
konrad | 152a399567 | |
kolaente | ca88232109 | |
kolaente | 12d8b8f0fa | |
kolaente | 56a3215147 | |
kolaente | 989ab530dd | |
kolaente | 80f86527ed | |
kolaente | 87529b5e3e | |
kolaente | b6d0570e79 | |
konrad | e99aeb67c6 | |
konrad | f09e5c6f84 | |
konrad | 63a74ee7ac | |
konrad | aa5b510424 | |
konrad | 7ddbec7a6f | |
kolaente | ab19edfbd3 | |
konrad | 3b38e61ab8 | |
konrad | 96ad0fc31b | |
konrad | 66b6dbf4f3 | |
konrad | 70d6278540 | |
konrad | 068bfc942e | |
kolaente | a734f21ac2 | |
konrad | 3d27bb1438 | |
konrad | 815ec40696 | |
konrad | a4b8a44e47 | |
konrad | c8da860eab | |
konrad | dec5db7649 | |
konrad | c3cfc73840 | |
konrad | 2e2877156b | |
konrad | d309d5b3b6 | |
konrad | bcb8b08001 | |
konrad | 434856a44f | |
konrad | a92d711b00 | |
konrad | 4275a3acad | |
konrad | 0027b1f001 | |
konrad | 323f4c7ff4 | |
konrad | aaf5ca0ceb | |
konrad | f7314b439f | |
konrad | f27172cfd8 | |
konrad | d7fd1082a4 | |
konrad | 4b82af8ae8 | |
konrad | 772ed316cb | |
konrad | dbc3886706 | |
konrad | 6c1ecf55f9 | |
konrad | 030e2af33d | |
konrad | 24d2a6519f | |
konrad | a01f0a7b33 | |
konrad | 1b6a304ae8 | |
konrad | 3c5af74141 | |
konrad | b613208950 | |
konrad | 3822e4a09e | |
konrad | 8da6ca8e0b | |
konrad | 7dd2523a00 | |
konrad | a809cbd485 | |
konrad | f4b8d07d0e | |
konrad | 2df5cd1694 | |
konrad | d171d1f787 | |
konrad | df0482dac2 | |
konrad | 9c7d2ca000 | |
konrad | 7e40119014 | |
konrad | 310d15d078 | |
konrad | 97ebfc0207 | |
konrad | 547b44d608 | |
konrad | a315f71144 | |
konrad | bd261ca375 | |
konrad | 64ae3244ed | |
konrad | ac78d311a8 | |
konrad | f00f473df5 | |
konrad | 14651d198c | |
konrad | adf7700a8a | |
konrad | ed38925489 | |
konrad | d0202aea41 | |
konrad | aa6bc74405 | |
konrad | 6868833854 | |
konrad | 01d3c27d82 | |
konrad | 3f147ce7c5 | |
konrad | 224e795531 | |
konrad | d2476e061f | |
konrad | 1911ec4d5f | |
konrad | 82d596994a | |
konrad | 3996f32e04 | |
konrad | aaee574f8f | |
konrad | 50980a0fce | |
konrad | 782135e105 | |
konrad | baa53b8972 | |
konrad | ff6a1f6d59 | |
konrad | 48c9ab08c9 | |
konrad | aa1010d74a | |
konrad | b2e086c3e6 | |
konrad | ea3b6fcc43 | |
konrad | 9527d086b2 | |
konrad | ce73d46a8c | |
konrad | 7e7cb0d699 | |
konrad | 02c855ee52 |
79
.drone.yml
79
.drone.yml
|
@ -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
|
||||
|
|
|
@ -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*
|
|
@ -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.
|
15
Makefile
15
Makefile
|
@ -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:
|
||||
|
|
|
@ -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)
|
File diff suppressed because it is too large
Load Diff
|
@ -1,3 +1 @@
|
|||
{
|
||||
"API_URL": "http://localhost:8082/api/v1/"
|
||||
}
|
||||
{"API_URL": "/api/v1/"}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
<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">
|
||||
|
|
|
@ -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>
|
||||
<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')} })
|
||||
|
|
|
@ -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>
|
||||
<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">
|
||||
|
|
|
@ -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>
|
||||
<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')} })
|
||||
|
|
|
@ -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>
|
||||
<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">
|
||||
|
|
|
@ -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>
|
||||
<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')} })
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
<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">
|
||||
|
|
|
@ -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>
|
||||
<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')} })
|
||||
|
|
|
@ -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'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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é'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
})
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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=
|
4
main.go
4
main.go
|
@ -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"
|
||||
|
|
|
@ -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{})
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
-
|
||||
id: 1
|
||||
name: 'John Doe'
|
||||
username: 'user1'
|
||||
password: '1234'
|
||||
email: 'johndoe@example.com'
|
||||
is_admin: true
|
|
@ -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))
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package models
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
MainTest(m, "..")
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
105
models/user.go
105
models/user.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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"})
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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"})
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
||||
*/
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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.
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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: " "}
|
||||
}
|
|
@ -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
|
|
@ -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...)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
.DS_Store
|
||||
bin
|
||||
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
language: go
|
||||
|
||||
script:
|
||||
- go vet ./...
|
||||
- go test -v ./...
|
||||
|
||||
go:
|
||||
- 1.3
|
||||
- 1.4
|
||||
- 1.5
|
||||
- 1.6
|
||||
- 1.7
|
||||
- tip
|
|
@ -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)]
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 }
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue