Compare commits

...

65 Commits
v0.2 ... master

Author SHA1 Message Date
kolaente ddc3477efd
Make it work with modules 2021-05-16 00:39:09 +02:00
konrad 0b1212f428
fmt
the build was successful Details
2018-04-13 15:05:23 +02:00
konrad c097264bde
Made tests work again
the build failed Details
2018-04-13 15:03:36 +02:00
konrad 2bc349a33b
Added test fixtures 2018-04-13 15:03:22 +02:00
konrad 5673acdd60
Fixed lint 2018-04-13 14:41:31 +02:00
konrad 0b4891c3ef
Added translations for viewing logs
the build failed Details
2018-03-07 16:19:56 +01:00
kolaente db5e3a7cbc
When viewing logs via the ui, you can now click on the item to show it 2018-03-07 16:07:56 +01:00
konrad 0030372879
started adding view logs via ui
the build failed Details
2018-03-06 16:32:28 +01:00
konrad 3de6ecf579
Added viewing logs via api 2018-03-06 15:33:18 +01:00
konrad 91e10eb78b
Fixed doer missing
the build failed Details
2018-03-06 14:01:46 +01:00
konrad 5fbc08cf65
fmt
the build failed Details
2018-03-06 13:43:07 +01:00
konrad 0763ae07b0
Properly implemented logging
the build failed Details
2018-03-06 12:36:49 +01:00
konrad d553ca743b
Moved to new namespace
the build was successful Details
2018-03-05 12:53:12 +01:00
konrad 8565772ab0
Added package-log.json 2018-03-05 12:49:26 +01:00
konrad cfb68b9f79
Updated makefile to work with go 1.10
the build was successful Details
2018-03-05 12:12:20 +01:00
konrad 7a3bc05243
Fixed typo in readme
the build was successful Details
2018-03-05 12:10:10 +01:00
kolaente 5b7fb1be87
Merge remote-tracking branch 'origin/master'
the build failed Details
2018-03-05 11:09:59 +01:00
konrad 0bf37c2ac0
Fixed build 2018-03-05 11:09:41 +01:00
konrad eebb8eb4c4 'Readme.md' ändern
the build failed Details
2018-03-04 22:51:43 +00:00
konrad d46cbc2d67
fmt
the build failed Details
2018-01-30 12:44:22 +01:00
konrad 537aebc873
Added tests for deleting the last user
the build failed Details
2018-01-30 12:40:47 +01:00
konrad 152a399567
Don't delete a user if its the last one
the build failed Details
2018-01-30 12:22:00 +01:00
kolaente ca88232109
Fixed cors 2018-01-30 11:23:00 +01:00
kolaente 12d8b8f0fa
Added frontend links to manage users + view logs
the build failed Details
2018-01-29 16:38:59 +01:00
kolaente 56a3215147
Return 400 instead of 500 when deleting or showing something where the id is not an int
the build failed Details
2018-01-29 16:04:20 +01:00
kolaente 989ab530dd
Return 400 instead of 500 when updateing something where the id is not an int 2018-01-29 15:46:56 +01:00
kolaente 80f86527ed
Obfuscated user passwords when returning a user object 2018-01-29 15:33:38 +01:00
kolaente 87529b5e3e
Updated Readme
the build was successful Details
2018-01-27 16:47:12 +01:00
kolaente b6d0570e79
Updated gitignore
the build was successful Details
2018-01-27 14:00:23 +01:00
konrad e99aeb67c6
fmt
the build was successful Details
2018-01-26 16:44:58 +01:00
konrad f09e5c6f84
Added check if the emailaddress is already used when creating a new user 2018-01-26 16:00:18 +01:00
konrad 63a74ee7ac
Fixed a bug where a user couldn't update its own password
the build failed Details
2018-01-26 15:09:33 +01:00
konrad aa5b510424
Improved unit tests
the build failed Details
2018-01-26 12:37:11 +01:00
konrad 7ddbec7a6f
Added more unit tests for authors 2018-01-26 12:29:16 +01:00
kolaente ab19edfbd3
updated gitignore
the build was successful Details
2018-01-25 22:31:50 +01:00
konrad 3b38e61ab8
Updated gitignore
the build was successful Details
2018-01-25 16:33:34 +01:00
konrad 96ad0fc31b
Small improvements to unit tests
the build was successful Details
2018-01-25 16:28:02 +01:00
konrad 66b6dbf4f3
Small improvements to unit tests 2018-01-25 14:39:27 +01:00
konrad 70d6278540
removed unused lib
the build was successful Details
2018-01-25 12:27:48 +01:00
konrad 068bfc942e
Beautified error messages
the build failed Details
2018-01-25 12:23:51 +01:00
kolaente a734f21ac2
Added custom error type when no username and password are given when creating a new user 2018-01-25 12:15:12 +01:00
konrad 3d27bb1438
Added custom error type when a publisher has no name
the build failed Details
2018-01-24 23:55:10 +01:00
konrad 815ec40696
removed unused lib 2018-01-24 13:20:46 +01:00
konrad a4b8a44e47
Added custom error for could not get User ID from JWT 2018-01-24 13:18:17 +01:00
konrad c8da860eab
Modified http status code on error 2018-01-24 13:15:28 +01:00
konrad dec5db7649
Added custom error type for no book title 2018-01-24 13:13:55 +01:00
konrad c3cfc73840
Added error type for no item title 2018-01-24 13:04:47 +01:00
konrad 2e2877156b
Added custom error type for ID cannot be 0 2018-01-24 12:58:00 +01:00
konrad d309d5b3b6
fixed lint + gofmt
the build was successful Details
2018-01-23 16:20:37 +01:00
konrad bcb8b08001
Added check if the wants to change its own password
the build failed Details
2018-01-23 15:58:01 +01:00
konrad 434856a44f
Added route to update a user's password
the build failed Details
2018-01-23 15:53:38 +01:00
konrad a92d711b00
Added admincheck to all adminroutes 2018-01-23 15:27:08 +01:00
konrad 4275a3acad
Added method to delete a user 2018-01-23 15:25:41 +01:00
konrad 0027b1f001
Added method to get information about a user
the build failed Details
2018-01-23 15:20:02 +01:00
konrad 323f4c7ff4
Added custom error type for user does not exist 2018-01-23 15:19:39 +01:00
konrad aaf5ca0ceb
Added custom error type for no username provided 2018-01-23 14:52:03 +01:00
konrad f7314b439f
gofmt
the build was successful Details
2018-01-23 14:32:23 +01:00
konrad f27172cfd8
Added method to add or update a user 2018-01-23 14:31:54 +01:00
konrad d7fd1082a4
Fixed lint 2018-01-23 13:00:32 +01:00
konrad 4b82af8ae8
Added method to update a user 2018-01-23 12:59:48 +01:00
konrad 772ed316cb
Added list user method 2018-01-23 12:37:13 +01:00
konrad dbc3886706
Added admin type user 2018-01-23 11:20:22 +01:00
konrad 6c1ecf55f9
Updated Systemd Template
the build was successful Details
2018-01-22 16:05:54 +01:00
konrad 030e2af33d
Added systemd template
the build was successful Details
2018-01-16 18:12:33 +01:00
konrad 24d2a6519f
Updated version
the build was successful Details
2018-01-16 17:17:59 +01:00
401 changed files with 100069 additions and 22884 deletions

View File

@ -1,6 +1,6 @@
workspace:
base: /srv/app
path: src/git.mowie.cc/konrad/Library
path: src/git.kolaente.de/konrad/Library
clone:
git:
@ -25,7 +25,7 @@ pipeline:
event: [ push, tag, pull_request ]
build-frontend:
image: webhippie/nodejs:current
image: node:latest
pull: true
group: build
commands:

12
.gitignore vendored
View File

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

View File

@ -1,5 +1,5 @@
DIST := dist
IMPORT := git.mowie.cc/konrad/Library
IMPORT := git.kolaente.de/konrad/Library
SED_INPLACE := sed -i
@ -21,7 +21,7 @@ EXTRA_GOFLAGS ?=
LDFLAGS := -X "main.Version=$(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')" -X "main.Tags=$(TAGS)"
PACKAGES ?= $(filter-out git.mowie.cc/konrad/Library/integrations,$(shell go list ./... | grep -v /vendor/))
PACKAGES ?= $(filter-out git.kolaente.de/konrad/Library/integrations,$(shell go list ./... | grep -v /vendor/))
SOURCES ?= $(shell find . -name "*.go" -type f)
TAGS ?=
@ -57,7 +57,7 @@ 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:

View File

@ -1,12 +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)
[![Downaload](https://img.shields.io/badge/download-v0.1.1-brightgreen.svg)](https://storage.kolaente.de/minio/library-release/)
[![Download](https://img.shields.io/badge/download-v0.2-brightgreen.svg)](https://storage.kolaente.de/minio/library-release/)
An application to manage your library, books and authors.
API Docs: https://git.mowie.cc/konrad/Library/wiki/API
API Docs: https://git.kolaente.de/konrad/Library/wiki/API
Download the latest release: https://storage.kolaente.de/minio/library-release/
(`master` is up-to-date with the master branch and can contain bugs, if you want a stable version, choose a version from the list)

11638
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -8,6 +8,8 @@
<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>
@ -51,6 +53,8 @@ export default {
router.push({ name: 'login' })
}
console.log(this.user.infos)
// Set the users avatar
this.setAvatar()
},

View File

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

View File

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

View File

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

View File

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

View File

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

32
go.mod Normal file
View File

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

72
go.sum Normal file
View File

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

View File

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

View File

@ -11,17 +11,22 @@ func TestAddOrUpdateAuthor(t *testing.T) {
// 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)
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)
author2, err := AddOrUpdateAuthor(testauthor, &doer)
assert.NoError(t, err)
assert.Equal(t, testauthor.Forename, author2.Forename)
assert.Equal(t, testauthor.Lastname, author2.Lastname)
@ -35,6 +40,15 @@ func TestAddOrUpdateAuthor(t *testing.T) {
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)
@ -43,23 +57,67 @@ func TestAddOrUpdateAuthor(t *testing.T) {
assert.Equal(t, gotauthor.Lastname, testauthor.Lastname)
// Pass an empty author to see if it fails
_, err = AddOrUpdateAuthor(Author{})
_, 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)
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)
err = DeleteAuthorByID(author1.ID, &doer)
assert.NoError(t, err)
// Check if it is gone
_, exists, err = GetAuthorByID(author1.ID)
assert.NoError(t, err)
assert.False(t, exists)
// Try deleting an author with ID = 0
err = DeleteAuthorByID(0, &doer)
assert.Error(t, err)
assert.True(t, IsErrIDCannotBeZero(err))
// =======================
// Testing without a table
// Drop the table to see it fail
x.DropTables(Author{})
// Test inserting
_, err = AddOrUpdateAuthor(Author{Forename: "ff", Lastname: "fff"}, &doer)
assert.Error(t, err)
// Test updating
_, err = AddOrUpdateAuthor(Author{ID: 3, Forename: "ff", Lastname: "fff"}, &doer)
assert.Error(t, err)
// Delete from some nonexistent
err = DeleteAuthorByID(3, &doer)
assert.Error(t, err)
// And get from nonexistant
_, err = ListAuthors("")
assert.Error(t, err)
//Aaaaaaaaaand recreate it
x.Sync(Author{})
}
func TestGetAuthorsByBook(t *testing.T) {
// Create test database
assert.NoError(t, PrepareTestDatabase())
// Drop the table to see it fail
x.DropTables(AuthorBook{})
_, err := GetAuthorsByBook(Book{ID: 1})
assert.Error(t, err)
//Aaaaaaaaaand recreate it
x.Sync(AuthorBook{})
}

View File

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

View File

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

View File

@ -9,9 +9,13 @@ 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)
testauthorin1, err := AddOrUpdateAuthor(testauthor1, &doer)
assert.NoError(t, err)
// Bootstrap our test book
@ -44,7 +48,7 @@ func TestAddOrUpdateBook(t *testing.T) {
}
// Insert one new Testbook
book1, err := AddOrUpdateBook(testbook)
book1, err := AddOrUpdateBook(testbook, &doer)
assert.NoError(t, err)
// Check if everything was inserted correctly
@ -67,7 +71,7 @@ func TestAddOrUpdateBook(t *testing.T) {
assert.Equal(t, book1.Authors[2].Forename, testauthor1.Forename)
// And anotherone
book2, err := AddOrUpdateBook(testbook)
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
@ -79,6 +83,14 @@ func TestAddOrUpdateBook(t *testing.T) {
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)
@ -86,13 +98,14 @@ func TestAddOrUpdateBook(t *testing.T) {
assert.Equal(t, testbook.Title, gotBook.Title)
// Pass an empty Book to see if it fails
_, err = AddOrUpdateBook(Book{})
_, 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)
book1updated, err := AddOrUpdateBook(testbook, &doer)
assert.NoError(t, err)
assert.Equal(t, testbook.Title, book1updated.Title)
@ -119,11 +132,16 @@ func TestAddOrUpdateBook(t *testing.T) {
assert.Equal(t, int64(99), qty2)
// Delete the book
err = DeleteBookByID(book1.ID)
err = DeleteBookByID(book1.ID, &doer)
assert.NoError(t, err)
// Check if its gone
_, exists, err = GetBookByID(book1.ID)
assert.NoError(t, err)
assert.False(t, exists)
// Try deleting one with ID = 0
err = DeleteBookByID(0, &doer)
assert.Error(t, err)
assert.True(t, IsErrIDCannotBeZero(err))
}

View File

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

View File

@ -1,12 +1,10 @@
package models
import "fmt"
// DeleteBookByID deletes a book by its ID
func DeleteBookByID(id int64) error {
func DeleteBookByID(id int64, doer *User) error {
// Check if the id is 0
if id == 0 {
return fmt.Errorf("ID cannot be 0")
return ErrIDCannotBeZero{}
}
// Delete the book
@ -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
}

View File

@ -11,10 +11,39 @@ func TestSetConfig(t *testing.T) {
// Create test database
assert.NoError(t, PrepareTestDatabase())
// Write a fake config
configString := `[General]
// 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 = :8080
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
@ -25,7 +54,7 @@ Name = nope
Username = user
Password = 1234
Email = nope@none.com`
err := ioutil.WriteFile("config.ini", []byte(configString), 0644)
err = ioutil.WriteFile("config.ini", []byte(configString), 0644)
assert.NoError(t, err)
// Test setConfig

177
models/error.go Normal file
View File

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

View File

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

View File

@ -9,6 +9,10 @@ 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",
@ -18,7 +22,7 @@ func TestAddOrUpdateItem(t *testing.T) {
}
// Create a new item
item1, err := AddOrUpdateItem(testitem)
item1, err := AddOrUpdateItem(testitem, &doer)
assert.NoError(t, err)
assert.Equal(t, testitem.Title, item1.Title)
assert.Equal(t, testitem.Price, item1.Price)
@ -26,7 +30,7 @@ func TestAddOrUpdateItem(t *testing.T) {
assert.Equal(t, testitem.Other, item1.Other)
// And anotherone
item2, err := AddOrUpdateItem(testitem)
item2, err := AddOrUpdateItem(testitem, &doer)
assert.NoError(t, err)
assert.Equal(t, testitem.Title, item2.Title)
@ -41,6 +45,17 @@ func TestAddOrUpdateItem(t *testing.T) {
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)
@ -51,13 +66,14 @@ func TestAddOrUpdateItem(t *testing.T) {
assert.Equal(t, testitem.Other, gotitem.Other)
// Pass an empty item to see if it fails
_, err = AddOrUpdateItem(Item{})
_, 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)
item1updated, err := AddOrUpdateItem(testitem, &doer)
assert.NoError(t, err)
assert.Equal(t, testitem.Title, item1updated.Title)
assert.Equal(t, testitem.Price, item1updated.Price)
@ -78,11 +94,16 @@ func TestAddOrUpdateItem(t *testing.T) {
assert.Equal(t, int64(99), qty2)
// Delete the item
err = DeleteItemByID(item1.ID)
err = DeleteItemByID(item1.ID, &doer)
assert.NoError(t, err)
// Check if it is gone
_, exists, err = GetItemByID(item1.ID)
assert.NoError(t, err)
assert.False(t, exists)
// Try deleting one with ID = 0
err = DeleteItemByID(0, &doer)
assert.Error(t, err)
assert.True(t, IsErrIDCannotBeZero(err))
}

View File

@ -1,15 +1,13 @@
package models
import "fmt"
// 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
return Item{}, fmt.Errorf("You need at least a title to create an item")
return Item{}, ErrItemTitleCannotBeEmpty{}
}
_, err = x.Insert(&item)
@ -17,12 +15,24 @@ func AddOrUpdateItem(item Item) (newItem Item, err error) {
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)
if err != nil {
return Item{}, err
}
// Log
err = logAction(ActionTypeItemUpdated, doer, item.ID)
if err != nil {
return Item{}, err
}
}
// Set the Quantity

View File

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

41
models/log_action.go Normal file
View File

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

View File

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

View File

@ -9,6 +9,10 @@ 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",
@ -21,7 +25,7 @@ func TestAddOrUpdatePublisher(t *testing.T) {
for _, publisher := range allpublishers {
// Delete
err = DeletePublisherByID(publisher.ID)
err = DeletePublisherByID(publisher.ID, &doer)
assert.NoError(t, err)
// Check if it is gone
@ -31,7 +35,7 @@ func TestAddOrUpdatePublisher(t *testing.T) {
}
// Create a new publisher
publisher1, err := AddOrUpdatePublisher(testpublisher)
publisher1, err := AddOrUpdatePublisher(testpublisher, &doer)
assert.NoError(t, err)
assert.Equal(t, testpublisher.Name, publisher1.Name)
@ -42,22 +46,33 @@ func TestAddOrUpdatePublisher(t *testing.T) {
assert.Equal(t, testpublisher.Name, gotpublisher.Name)
// Pass an empty publisher to see if it fails
_, err = AddOrUpdatePublisher(Publisher{})
_, 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)
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)
err = DeletePublisherByID(publisher1.ID, &doer)
assert.NoError(t, err)
// Check if it is gone
_, exists, err = GetPublisherByID(publisher1.ID)
assert.NoError(t, err)
assert.False(t, exists)
// Try deleting one with ID = 0
err = DeletePublisherByID(0, &doer)
assert.Error(t, err)
assert.True(t, IsErrIDCannotBeZero(err))
}

View File

@ -1,22 +1,30 @@
package models
import "fmt"
// 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
return Publisher{}, fmt.Errorf("You need at least a name to insert a new publisher")
return Publisher{}, ErrNoPublisherName{}
}
_, err = x.Insert(&publisher)
if err != nil {
return Publisher{}, err
}
// Log
err = logAction(ActionTypePublisherAdded, doer, publisher.ID)
if err != nil {
return Publisher{}, err
}
} else {
_, err = x.ID(publisher.ID).Update(&publisher)
if err != nil {
return Publisher{}, err
}
// Log
err = logAction(ActionTypePublisherUpdated, doer, publisher.ID)
if err != nil {
return Publisher{}, err
}

View File

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

View File

@ -13,7 +13,7 @@ import (
// MainTest creates the test engine
func MainTest(m *testing.M, pathToRoot string) {
var err error
fixturesDir := filepath.Join(pathToRoot, "models")
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)
@ -25,6 +25,7 @@ func MainTest(m *testing.M, pathToRoot string) {
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
}

View File

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

134
models/user_add_update.go Normal file
View File

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

31
models/user_delete.go Normal file
View File

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

View File

@ -9,40 +9,144 @@ 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: "testu",
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)
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
createdUser, err = CreateUser(dummyuser)
_, err = CreateUser(dummyuser, &doer)
assert.Error(t, err)
// Check if it fails to create a user with just the same username
createdUser, err = CreateUser(User{Username: "testuu"})
_, 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
createdUser, err = CreateUser(User{})
_, err = CreateUser(User{}, &doer)
assert.Error(t, err)
assert.True(t, IsErrNoUsernamePassword(err))
createdUser, err = CreateUser(User{Name: "blub"})
_, err = CreateUser(User{Name: "blub"}, &doer)
assert.Error(t, err)
assert.True(t, IsErrNoUsernamePassword(err))
// Check if he exists
_, exists, err := GetUser(createdUser)
theuser, exists, err := GetUser(createdUser)
assert.NoError(t, err)
assert.True(t, exists)
// Get by his ID
_, exists, err = GetUserByID(theuser.ID)
assert.NoError(t, err)
assert.True(t, exists)
// Passing 0 as ID should return an empty user
_, exists, err = GetUserByID(0)
assert.NoError(t, err)
assert.False(t, exists)
// Check the user credentials
user, err := CheckUserCredentials(&UserLogin{"testuu", "1234"})
assert.NoError(t, err)
assert.Equal(t, dummyuser.Name, user.Name)
// Check wrong password (should also fail)
_, err = CheckUserCredentials(&UserLogin{"testuu", "12345"})
assert.Error(t, err)
// Check usercredentials for a nonexistent user (should fail)
_, err = CheckUserCredentials(&UserLogin{"dfstestuu", "1234"})
assert.Error(t, err)
assert.True(t, IsErrUserDoesNotExist(err))
// Update the user
newname := "Test_te"
uuser, err := UpdateUser(User{ID: theuser.ID, Name: newname, Password: "444444"}, &doer)
assert.NoError(t, err)
assert.Equal(t, newname, uuser.Name)
assert.Equal(t, theuser.Password, uuser.Password) // Password should not change
assert.Equal(t, theuser.Username, uuser.Username) // Username should not change either
// Try updating one which does not exist
_, err = UpdateUser(User{ID: 99999, Username: "dg"}, &doer)
assert.Error(t, err)
assert.True(t, IsErrUserDoesNotExist(err))
// Update a users password
newpassword := "55555"
err = UpdateUserPassword(theuser.ID, newpassword, &doer)
assert.NoError(t, err)
// Check if it was changed
user, err = CheckUserCredentials(&UserLogin{theuser.Username, newpassword})
assert.NoError(t, err)
assert.Equal(t, newname, user.Name)
// Check if the searchterm works
all, err := ListUsers("test")
assert.NoError(t, err)
assert.True(t, len(all) > 0)
all, err = ListUsers("")
assert.NoError(t, err)
assert.True(t, len(all) > 0)
// Try updating the password of a nonexistent user (should fail)
err = UpdateUserPassword(9999, newpassword, &doer)
assert.Error(t, err)
assert.True(t, IsErrUserDoesNotExist(err))
// Delete it
err = DeleteUserByID(theuser.ID, &doer)
assert.NoError(t, err)
// Try deleting one with ID = 0
err = DeleteUserByID(0, &doer)
assert.Error(t, err)
assert.True(t, IsErrIDCannotBeZero(err))
// Try delete the last user (Should fail)
err = DeleteUserByID(createdUser2.ID, &doer)
assert.Error(t, err)
assert.True(t, IsErrCannotDeleteLastUser(err))
// Log some user action
err = logAction(ActionTypeUserUpdated, &doer, 1)
assert.NoError(t, err)
}

25
models/users_list.go Normal file
View File

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

View File

@ -1,7 +1,7 @@
package v1
import (
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
"strconv"
@ -30,17 +30,20 @@ func AuthorDelete(c echo.Context) error {
return c.JSON(http.StatusNotFound, models.Message{"The author does not exist."})
}
// Delete it
err = models.DeleteAuthorByID(authorID)
// Get the user options
doer, err := models.GetCurrentUser(c)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not delete author."})
return err
}
// Log the action
err = models.LogAction("Deleted an author", authorID, c)
// Delete it
err = models.DeleteAuthorByID(authorID, &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 author."})
}
return c.JSON(http.StatusOK, models.Message{"success"})

View File

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

View File

@ -2,7 +2,7 @@ package v1
import (
"encoding/json"
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
"strconv"
@ -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,7 +41,7 @@ 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
}
@ -58,17 +58,20 @@ func AuthorAddOrUpdate(c echo.Context) error {
}
}
// Insert or update the author
newAuthor, err := models.AddOrUpdateAuthor(*datAuthor)
// Get the user options
doer, err := models.GetCurrentUser(c)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Error"})
return err
}
// Log the action
err = models.LogAction("Added or updated an author", newAuthor.ID, c)
// Insert or update the author
newAuthor, err := models.AddOrUpdateAuthor(*datAuthor, &doer)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not log."})
if models.IsErrAuthorCannotBeEmpty(err) {
return c.JSON(http.StatusBadRequest, models.Message{"Please provide at least a name and a lastname."})
}
return c.JSON(http.StatusInternalServerError, models.Message{"Error"})
}
return c.JSON(http.StatusOK, newAuthor)

View File

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

View File

@ -1,7 +1,7 @@
package v1
import (
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
"strconv"
@ -30,17 +30,20 @@ func BookDelete(c echo.Context) error {
return c.JSON(http.StatusNotFound, models.Message{"The book does not exist."})
}
// Delete it
err = models.DeleteBookByID(bookID)
// Get the user options
doer, err := models.GetCurrentUser(c)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not delete book."})
return err
}
// Log the action
err = models.LogAction("Deleted a book", bookID, c)
// Delete it
err = models.DeleteBookByID(bookID, &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 book."})
}
return c.JSON(http.StatusOK, models.Message{"success"})

View File

@ -1,7 +1,7 @@
package v1
import (
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
"strconv"
@ -17,6 +17,9 @@ 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)

View File

@ -2,7 +2,7 @@ package v1
import (
"encoding/json"
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
"strconv"
@ -36,7 +36,7 @@ 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
}
@ -58,18 +58,43 @@ func BookAddOrUpdate(c echo.Context) error {
return c.JSON(http.StatusBadRequest, models.Message{"You need at least a title to insert a new book!"})
}
// Get the user options
doer, err := models.GetCurrentUser(c)
if err != nil {
return err
}
// Insert or update the book
newBook, err := models.AddOrUpdateBook(*datBook)
newBook, err := models.AddOrUpdateBook(*datBook, &doer)
if err != nil {
if models.IsErrAuthorCannotBeEmpty(err) {
return c.JSON(http.StatusBadRequest, models.Message{"Id cannot be 0."})
}
if models.IsErrBookTitleCannotBeEmpty(err) {
return c.JSON(http.StatusBadRequest, models.Message{"You need to provide at least a title for the book."})
}
if models.IsErrNoPublisherName(err) {
return c.JSON(http.StatusBadRequest, models.Message{"You need to provide at least a name to insert a new publisher."})
}
return c.JSON(http.StatusInternalServerError, models.Message{"Error"})
}
// Log the action
err = models.LogAction("Added or updated a book", newBook.ID, c)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not log."})
}
/*if datBook.ID == 0 { // If the ID is, the author was added, otherwise updated
err = models.LogAction(models.ActionTypeBookAdded, newBook.ID, c)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not log."})
}
} else {
err = models.LogAction(models.ActionTypeBookUpdated, newBook.ID, c)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not log."})
}
}*/
return c.JSON(http.StatusOK, newBook)
}

View File

@ -5,7 +5,7 @@ import (
"net/http"
"fmt"
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
)
// BookList is the handler to list books

View File

@ -2,7 +2,7 @@ package v1
import (
"encoding/json"
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
"strconv"
@ -36,7 +36,7 @@ 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
}
@ -53,17 +53,20 @@ func ItemAddOrUpdate(c echo.Context) error {
}
}
// Insert or update the item
newItem, err := models.AddOrUpdateItem(*datItem)
// Get the user options
doer, err := models.GetCurrentUser(c)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Error"})
return err
}
// Log the action
err = models.LogAction("Added or updated an item", newItem.ID, c)
// Insert or update the item
newItem, err := models.AddOrUpdateItem(*datItem, &doer)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not log."})
if models.IsErrItemTitleCannotBeEmpty(err) {
return c.JSON(http.StatusInternalServerError, models.Message{"Please provide at least a title for the item."})
}
return c.JSON(http.StatusInternalServerError, models.Message{"Error"})
}
return c.JSON(http.StatusOK, newItem)

View File

@ -1,7 +1,7 @@
package v1
import (
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
"strconv"
@ -30,17 +30,20 @@ func ItemDelete(c echo.Context) error {
return c.JSON(http.StatusBadRequest, models.Message{"The item does not exist."})
}
// Delete it
err = models.DeleteItemByID(itemID)
// Get the user options
doer, err := models.GetCurrentUser(c)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not delete item."})
return err
}
// Log the action
err = models.LogAction("Deleted an item", itemID, c)
// Delete it
err = models.DeleteItemByID(itemID, &doer)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not log."})
if models.IsErrIDCannotBeZero(err) {
return c.JSON(http.StatusBadRequest, models.Message{"Id cannot be 0"})
}
return c.JSON(http.StatusInternalServerError, models.Message{"Could not delete item."})
}
return c.JSON(http.StatusOK, models.Message{"success"})

View File

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

View File

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

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

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

View File

@ -2,7 +2,7 @@ package v1
import (
"encoding/json"
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
"strconv"
@ -37,7 +37,7 @@ 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
}
@ -54,18 +54,35 @@ func PublisherAddOrUpdate(c echo.Context) error {
}
}
// Get the user options
doer, err := models.GetCurrentUser(c)
if err != nil {
return err
}
// Insert or update the publisher
newPublisher, err := models.AddOrUpdatePublisher(*datPublisher)
newPublisher, err := models.AddOrUpdatePublisher(*datPublisher, &doer)
if err != nil {
if models.IsErrNoPublisherName(err) {
return c.JSON(http.StatusBadRequest, models.Message{"You need to provide at least a name to insert a new publisher."})
}
return c.JSON(http.StatusInternalServerError, models.Message{"Error"})
}
// Log the action
err = models.LogAction("Added or updated a publisher", newPublisher.ID, c)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not log."})
}
/*if datPublisher.ID == 0 { // If the ID is, the author was added, otherwise updated
err = models.LogAction(models.ActionTypePublisherAdded, newPublisher.ID, c)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not log."})
}
} else {
err = models.LogAction(models.ActionTypePublisherUpdated, newPublisher.ID, c)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not log."})
}
}*/
return c.JSON(http.StatusOK, newPublisher)
}

View File

@ -1,7 +1,7 @@
package v1
import (
"git.mowie.cc/konrad/Library/models"
"git.kolaente.de/konrad/Library/models"
"github.com/labstack/echo"
"net/http"
"strconv"
@ -30,18 +30,27 @@ func PublisherDelete(c echo.Context) error {
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 {
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.StatusOK, models.Message{"success"})
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

15
systemd/library.service Normal file
View File

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

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

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

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

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

View File

@ -1,175 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,61 +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
// Error represents an error returned in a command reply.
type Error string
func (err Error) Error() string { return string(err) }
// Conn represents a connection to a Redis server.
type Conn interface {
// Close closes the connection.
Close() error
// Err returns a non-nil value when the connection is not usable.
Err() error
// Do sends a command to the server and returns the received reply.
Do(commandName string, args ...interface{}) (reply interface{}, err error)
// Send writes the command to the client's output buffer.
Send(commandName string, args ...interface{}) error
// Flush flushes the output buffer to the Redis server.
Flush() error
// Receive receives a single reply from the Redis server
Receive() (reply interface{}, err error)
}
// Argument is the interface implemented by an object which wants to control how
// the object is converted to Redis bulk strings.
type Argument interface {
// RedisArg returns a value to be encoded as a bulk string per the
// conversions listed in the section 'Executing Commands'.
// Implementations should typically return a []byte or string.
RedisArg() interface{}
}
// Scanner is implemented by an object which wants to control its value is
// interpreted when read from Redis.
type Scanner interface {
// RedisScan assigns a value from a Redis value. The argument src is one of
// the reply types listed in the section `Executing Commands`.
//
// An error should be returned if the value cannot be stored without
// loss of information.
RedisScan(src interface{}) error
}

View File

@ -1,479 +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"
"fmt"
"strconv"
)
// ErrNil indicates that a reply value is nil.
var ErrNil = errors.New("redigo: nil returned")
// Int is a helper that converts a command reply to an integer. If err is not
// equal to nil, then Int returns 0, err. Otherwise, Int converts the
// reply to an int as follows:
//
// Reply type Result
// integer int(reply), nil
// bulk string parsed reply, nil
// nil 0, ErrNil
// other 0, error
func Int(reply interface{}, err error) (int, error) {
if err != nil {
return 0, err
}
switch reply := reply.(type) {
case int64:
x := int(reply)
if int64(x) != reply {
return 0, strconv.ErrRange
}
return x, nil
case []byte:
n, err := strconv.ParseInt(string(reply), 10, 0)
return int(n), err
case nil:
return 0, ErrNil
case Error:
return 0, reply
}
return 0, fmt.Errorf("redigo: unexpected type for Int, got type %T", reply)
}
// Int64 is a helper that converts a command reply to 64 bit integer. If err is
// not equal to nil, then Int returns 0, err. Otherwise, Int64 converts the
// reply to an int64 as follows:
//
// Reply type Result
// integer reply, nil
// bulk string parsed reply, nil
// nil 0, ErrNil
// other 0, error
func Int64(reply interface{}, err error) (int64, error) {
if err != nil {
return 0, err
}
switch reply := reply.(type) {
case int64:
return reply, nil
case []byte:
n, err := strconv.ParseInt(string(reply), 10, 64)
return n, err
case nil:
return 0, ErrNil
case Error:
return 0, reply
}
return 0, fmt.Errorf("redigo: unexpected type for Int64, got type %T", reply)
}
var errNegativeInt = errors.New("redigo: unexpected value for Uint64")
// Uint64 is a helper that converts a command reply to 64 bit integer. If err is
// not equal to nil, then Int returns 0, err. Otherwise, Int64 converts the
// reply to an int64 as follows:
//
// Reply type Result
// integer reply, nil
// bulk string parsed reply, nil
// nil 0, ErrNil
// other 0, error
func Uint64(reply interface{}, err error) (uint64, error) {
if err != nil {
return 0, err
}
switch reply := reply.(type) {
case int64:
if reply < 0 {
return 0, errNegativeInt
}
return uint64(reply), nil
case []byte:
n, err := strconv.ParseUint(string(reply), 10, 64)
return n, err
case nil:
return 0, ErrNil
case Error:
return 0, reply
}
return 0, fmt.Errorf("redigo: unexpected type for Uint64, got type %T", reply)
}
// Float64 is a helper that converts a command reply to 64 bit float. If err is
// not equal to nil, then Float64 returns 0, err. Otherwise, Float64 converts
// the reply to an int as follows:
//
// Reply type Result
// bulk string parsed reply, nil
// nil 0, ErrNil
// other 0, error
func Float64(reply interface{}, err error) (float64, error) {
if err != nil {
return 0, err
}
switch reply := reply.(type) {
case []byte:
n, err := strconv.ParseFloat(string(reply), 64)
return n, err
case nil:
return 0, ErrNil
case Error:
return 0, reply
}
return 0, fmt.Errorf("redigo: unexpected type for Float64, got type %T", reply)
}
// String is a helper that converts a command reply to a string. If err is not
// equal to nil, then String returns "", err. Otherwise String converts the
// reply to a string as follows:
//
// Reply type Result
// bulk string string(reply), nil
// simple string reply, nil
// nil "", ErrNil
// other "", error
func String(reply interface{}, err error) (string, error) {
if err != nil {
return "", err
}
switch reply := reply.(type) {
case []byte:
return string(reply), nil
case string:
return reply, nil
case nil:
return "", ErrNil
case Error:
return "", reply
}
return "", fmt.Errorf("redigo: unexpected type for String, got type %T", reply)
}
// Bytes is a helper that converts a command reply to a slice of bytes. If err
// is not equal to nil, then Bytes returns nil, err. Otherwise Bytes converts
// the reply to a slice of bytes as follows:
//
// Reply type Result
// bulk string reply, nil
// simple string []byte(reply), nil
// nil nil, ErrNil
// other nil, error
func Bytes(reply interface{}, err error) ([]byte, error) {
if err != nil {
return nil, err
}
switch reply := reply.(type) {
case []byte:
return reply, nil
case string:
return []byte(reply), nil
case nil:
return nil, ErrNil
case Error:
return nil, reply
}
return nil, fmt.Errorf("redigo: unexpected type for Bytes, got type %T", reply)
}
// Bool is a helper that converts a command reply to a boolean. If err is not
// equal to nil, then Bool returns false, err. Otherwise Bool converts the
// reply to boolean as follows:
//
// Reply type Result
// integer value != 0, nil
// bulk string strconv.ParseBool(reply)
// nil false, ErrNil
// other false, error
func Bool(reply interface{}, err error) (bool, error) {
if err != nil {
return false, err
}
switch reply := reply.(type) {
case int64:
return reply != 0, nil
case []byte:
return strconv.ParseBool(string(reply))
case nil:
return false, ErrNil
case Error:
return false, reply
}
return false, fmt.Errorf("redigo: unexpected type for Bool, got type %T", reply)
}
// MultiBulk is a helper that converts an array command reply to a []interface{}.
//
// Deprecated: Use Values instead.
func MultiBulk(reply interface{}, err error) ([]interface{}, error) { return Values(reply, err) }
// Values is a helper that converts an array command reply to a []interface{}.
// If err is not equal to nil, then Values returns nil, err. Otherwise, Values
// converts the reply as follows:
//
// Reply type Result
// array reply, nil
// nil nil, ErrNil
// other nil, error
func Values(reply interface{}, err error) ([]interface{}, error) {
if err != nil {
return nil, err
}
switch reply := reply.(type) {
case []interface{}:
return reply, nil
case nil:
return nil, ErrNil
case Error:
return nil, reply
}
return nil, fmt.Errorf("redigo: unexpected type for Values, got type %T", reply)
}
func sliceHelper(reply interface{}, err error, name string, makeSlice func(int), assign func(int, interface{}) error) error {
if err != nil {
return err
}
switch reply := reply.(type) {
case []interface{}:
makeSlice(len(reply))
for i := range reply {
if reply[i] == nil {
continue
}
if err := assign(i, reply[i]); err != nil {
return err
}
}
return nil
case nil:
return ErrNil
case Error:
return reply
}
return fmt.Errorf("redigo: unexpected type for %s, got type %T", name, reply)
}
// Float64s is a helper that converts an array command reply to a []float64. If
// err is not equal to nil, then Float64s returns nil, err. Nil array items are
// converted to 0 in the output slice. Floats64 returns an error if an array
// item is not a bulk string or nil.
func Float64s(reply interface{}, err error) ([]float64, error) {
var result []float64
err = sliceHelper(reply, err, "Float64s", func(n int) { result = make([]float64, n) }, func(i int, v interface{}) error {
p, ok := v.([]byte)
if !ok {
return fmt.Errorf("redigo: unexpected element type for Floats64, got type %T", v)
}
f, err := strconv.ParseFloat(string(p), 64)
result[i] = f
return err
})
return result, err
}
// Strings is a helper that converts an array command reply to a []string. If
// err is not equal to nil, then Strings returns nil, err. Nil array items are
// converted to "" in the output slice. Strings returns an error if an array
// item is not a bulk string or nil.
func Strings(reply interface{}, err error) ([]string, error) {
var result []string
err = sliceHelper(reply, err, "Strings", func(n int) { result = make([]string, n) }, func(i int, v interface{}) error {
switch v := v.(type) {
case string:
result[i] = v
return nil
case []byte:
result[i] = string(v)
return nil
default:
return fmt.Errorf("redigo: unexpected element type for Strings, got type %T", v)
}
})
return result, err
}
// ByteSlices is a helper that converts an array command reply to a [][]byte.
// If err is not equal to nil, then ByteSlices returns nil, err. Nil array
// items are stay nil. ByteSlices returns an error if an array item is not a
// bulk string or nil.
func ByteSlices(reply interface{}, err error) ([][]byte, error) {
var result [][]byte
err = sliceHelper(reply, err, "ByteSlices", func(n int) { result = make([][]byte, n) }, func(i int, v interface{}) error {
p, ok := v.([]byte)
if !ok {
return fmt.Errorf("redigo: unexpected element type for ByteSlices, got type %T", v)
}
result[i] = p
return nil
})
return result, err
}
// Int64s is a helper that converts an array command reply to a []int64.
// If err is not equal to nil, then Int64s returns nil, err. Nil array
// items are stay nil. Int64s returns an error if an array item is not a
// bulk string or nil.
func Int64s(reply interface{}, err error) ([]int64, error) {
var result []int64
err = sliceHelper(reply, err, "Int64s", func(n int) { result = make([]int64, n) }, func(i int, v interface{}) error {
switch v := v.(type) {
case int64:
result[i] = v
return nil
case []byte:
n, err := strconv.ParseInt(string(v), 10, 64)
result[i] = n
return err
default:
return fmt.Errorf("redigo: unexpected element type for Int64s, got type %T", v)
}
})
return result, err
}
// Ints is a helper that converts an array command reply to a []in.
// If err is not equal to nil, then Ints returns nil, err. Nil array
// items are stay nil. Ints returns an error if an array item is not a
// bulk string or nil.
func Ints(reply interface{}, err error) ([]int, error) {
var result []int
err = sliceHelper(reply, err, "Ints", func(n int) { result = make([]int, n) }, func(i int, v interface{}) error {
switch v := v.(type) {
case int64:
n := int(v)
if int64(n) != v {
return strconv.ErrRange
}
result[i] = n
return nil
case []byte:
n, err := strconv.Atoi(string(v))
result[i] = n
return err
default:
return fmt.Errorf("redigo: unexpected element type for Ints, got type %T", v)
}
})
return result, err
}
// StringMap is a helper that converts an array of strings (alternating key, value)
// into a map[string]string. The HGETALL and CONFIG GET commands return replies in this format.
// Requires an even number of values in result.
func StringMap(result interface{}, err error) (map[string]string, error) {
values, err := Values(result, err)
if err != nil {
return nil, err
}
if len(values)%2 != 0 {
return nil, errors.New("redigo: StringMap expects even number of values result")
}
m := make(map[string]string, len(values)/2)
for i := 0; i < len(values); i += 2 {
key, okKey := values[i].([]byte)
value, okValue := values[i+1].([]byte)
if !okKey || !okValue {
return nil, errors.New("redigo: StringMap key not a bulk string value")
}
m[string(key)] = string(value)
}
return m, nil
}
// IntMap is a helper that converts an array of strings (alternating key, value)
// into a map[string]int. The HGETALL commands return replies in this format.
// Requires an even number of values in result.
func IntMap(result interface{}, err error) (map[string]int, error) {
values, err := Values(result, err)
if err != nil {
return nil, err
}
if len(values)%2 != 0 {
return nil, errors.New("redigo: IntMap expects even number of values result")
}
m := make(map[string]int, len(values)/2)
for i := 0; i < len(values); i += 2 {
key, ok := values[i].([]byte)
if !ok {
return nil, errors.New("redigo: IntMap key not a bulk string value")
}
value, err := Int(values[i+1], nil)
if err != nil {
return nil, err
}
m[string(key)] = value
}
return m, nil
}
// Int64Map is a helper that converts an array of strings (alternating key, value)
// into a map[string]int64. The HGETALL commands return replies in this format.
// Requires an even number of values in result.
func Int64Map(result interface{}, err error) (map[string]int64, error) {
values, err := Values(result, err)
if err != nil {
return nil, err
}
if len(values)%2 != 0 {
return nil, errors.New("redigo: Int64Map expects even number of values result")
}
m := make(map[string]int64, len(values)/2)
for i := 0; i < len(values); i += 2 {
key, ok := values[i].([]byte)
if !ok {
return nil, errors.New("redigo: Int64Map key not a bulk string value")
}
value, err := Int64(values[i+1], nil)
if err != nil {
return nil, err
}
m[string(key)] = value
}
return m, nil
}
// Positions is a helper that converts an array of positions (lat, long)
// into a [][2]float64. The GEOPOS command returns replies in this format.
func Positions(result interface{}, err error) ([]*[2]float64, error) {
values, err := Values(result, err)
if err != nil {
return nil, err
}
positions := make([]*[2]float64, len(values))
for i := range values {
if values[i] == nil {
continue
}
p, ok := values[i].([]interface{})
if !ok {
return nil, fmt.Errorf("redigo: unexpected element type for interface slice, got type %T", values[i])
}
if len(p) != 2 {
return nil, fmt.Errorf("redigo: unexpected number of values for a member position, got %d", len(p))
}
lat, err := Float64(p[0], nil)
if err != nil {
return nil, err
}
long, err := Float64(p[1], nil)
if err != nil {
return nil, err
}
positions[i] = &[2]float64{lat, long}
}
return positions, nil
}

View File

@ -1,585 +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"
"fmt"
"reflect"
"strconv"
"strings"
"sync"
)
func ensureLen(d reflect.Value, n int) {
if n > d.Cap() {
d.Set(reflect.MakeSlice(d.Type(), n, n))
} else {
d.SetLen(n)
}
}
func cannotConvert(d reflect.Value, s interface{}) error {
var sname string
switch s.(type) {
case string:
sname = "Redis simple string"
case Error:
sname = "Redis error"
case int64:
sname = "Redis integer"
case []byte:
sname = "Redis bulk string"
case []interface{}:
sname = "Redis array"
default:
sname = reflect.TypeOf(s).String()
}
return fmt.Errorf("cannot convert from %s to %s", sname, d.Type())
}
func convertAssignBulkString(d reflect.Value, s []byte) (err error) {
switch d.Type().Kind() {
case reflect.Float32, reflect.Float64:
var x float64
x, err = strconv.ParseFloat(string(s), d.Type().Bits())
d.SetFloat(x)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
var x int64
x, err = strconv.ParseInt(string(s), 10, d.Type().Bits())
d.SetInt(x)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
var x uint64
x, err = strconv.ParseUint(string(s), 10, d.Type().Bits())
d.SetUint(x)
case reflect.Bool:
var x bool
x, err = strconv.ParseBool(string(s))
d.SetBool(x)
case reflect.String:
d.SetString(string(s))
case reflect.Slice:
if d.Type().Elem().Kind() != reflect.Uint8 {
err = cannotConvert(d, s)
} else {
d.SetBytes(s)
}
default:
err = cannotConvert(d, s)
}
return
}
func convertAssignInt(d reflect.Value, s int64) (err error) {
switch d.Type().Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
d.SetInt(s)
if d.Int() != s {
err = strconv.ErrRange
d.SetInt(0)
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if s < 0 {
err = strconv.ErrRange
} else {
x := uint64(s)
d.SetUint(x)
if d.Uint() != x {
err = strconv.ErrRange
d.SetUint(0)
}
}
case reflect.Bool:
d.SetBool(s != 0)
default:
err = cannotConvert(d, s)
}
return
}
func convertAssignValue(d reflect.Value, s interface{}) (err error) {
if d.Kind() != reflect.Ptr {
if d.CanAddr() {
d2 := d.Addr()
if d2.CanInterface() {
if scanner, ok := d2.Interface().(Scanner); ok {
return scanner.RedisScan(s)
}
}
}
} else if d.CanInterface() {
// Already a reflect.Ptr
if d.IsNil() {
d.Set(reflect.New(d.Type().Elem()))
}
if scanner, ok := d.Interface().(Scanner); ok {
return scanner.RedisScan(s)
}
}
switch s := s.(type) {
case []byte:
err = convertAssignBulkString(d, s)
case int64:
err = convertAssignInt(d, s)
default:
err = cannotConvert(d, s)
}
return err
}
func convertAssignArray(d reflect.Value, s []interface{}) error {
if d.Type().Kind() != reflect.Slice {
return cannotConvert(d, s)
}
ensureLen(d, len(s))
for i := 0; i < len(s); i++ {
if err := convertAssignValue(d.Index(i), s[i]); err != nil {
return err
}
}
return nil
}
func convertAssign(d interface{}, s interface{}) (err error) {
if scanner, ok := d.(Scanner); ok {
return scanner.RedisScan(s)
}
// Handle the most common destination types using type switches and
// fall back to reflection for all other types.
switch s := s.(type) {
case nil:
// ignore
case []byte:
switch d := d.(type) {
case *string:
*d = string(s)
case *int:
*d, err = strconv.Atoi(string(s))
case *bool:
*d, err = strconv.ParseBool(string(s))
case *[]byte:
*d = s
case *interface{}:
*d = s
case nil:
// skip value
default:
if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr {
err = cannotConvert(d, s)
} else {
err = convertAssignBulkString(d.Elem(), s)
}
}
case int64:
switch d := d.(type) {
case *int:
x := int(s)
if int64(x) != s {
err = strconv.ErrRange
x = 0
}
*d = x
case *bool:
*d = s != 0
case *interface{}:
*d = s
case nil:
// skip value
default:
if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr {
err = cannotConvert(d, s)
} else {
err = convertAssignInt(d.Elem(), s)
}
}
case string:
switch d := d.(type) {
case *string:
*d = s
case *interface{}:
*d = s
case nil:
// skip value
default:
err = cannotConvert(reflect.ValueOf(d), s)
}
case []interface{}:
switch d := d.(type) {
case *[]interface{}:
*d = s
case *interface{}:
*d = s
case nil:
// skip value
default:
if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr {
err = cannotConvert(d, s)
} else {
err = convertAssignArray(d.Elem(), s)
}
}
case Error:
err = s
default:
err = cannotConvert(reflect.ValueOf(d), s)
}
return
}
// Scan copies from src to the values pointed at by dest.
//
// Scan uses RedisScan if available otherwise:
//
// The values pointed at by dest must be an integer, float, boolean, string,
// []byte, interface{} or slices of these types. Scan uses the standard strconv
// package to convert bulk strings to numeric and boolean types.
//
// If a dest value is nil, then the corresponding src value is skipped.
//
// If a src element is nil, then the corresponding dest value is not modified.
//
// To enable easy use of Scan in a loop, Scan returns the slice of src
// following the copied values.
func Scan(src []interface{}, dest ...interface{}) ([]interface{}, error) {
if len(src) < len(dest) {
return nil, errors.New("redigo.Scan: array short")
}
var err error
for i, d := range dest {
err = convertAssign(d, src[i])
if err != nil {
err = fmt.Errorf("redigo.Scan: cannot assign to dest %d: %v", i, err)
break
}
}
return src[len(dest):], err
}
type fieldSpec struct {
name string
index []int
omitEmpty bool
}
type structSpec struct {
m map[string]*fieldSpec
l []*fieldSpec
}
func (ss *structSpec) fieldSpec(name []byte) *fieldSpec {
return ss.m[string(name)]
}
func compileStructSpec(t reflect.Type, depth map[string]int, index []int, ss *structSpec) {
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
switch {
case f.PkgPath != "" && !f.Anonymous:
// Ignore unexported fields.
case f.Anonymous:
// TODO: Handle pointers. Requires change to decoder and
// protection against infinite recursion.
if f.Type.Kind() == reflect.Struct {
compileStructSpec(f.Type, depth, append(index, i), ss)
}
default:
fs := &fieldSpec{name: f.Name}
tag := f.Tag.Get("redis")
p := strings.Split(tag, ",")
if len(p) > 0 {
if p[0] == "-" {
continue
}
if len(p[0]) > 0 {
fs.name = p[0]
}
for _, s := range p[1:] {
switch s {
case "omitempty":
fs.omitEmpty = true
default:
panic(fmt.Errorf("redigo: unknown field tag %s for type %s", s, t.Name()))
}
}
}
d, found := depth[fs.name]
if !found {
d = 1 << 30
}
switch {
case len(index) == d:
// At same depth, remove from result.
delete(ss.m, fs.name)
j := 0
for i := 0; i < len(ss.l); i++ {
if fs.name != ss.l[i].name {
ss.l[j] = ss.l[i]
j += 1
}
}
ss.l = ss.l[:j]
case len(index) < d:
fs.index = make([]int, len(index)+1)
copy(fs.index, index)
fs.index[len(index)] = i
depth[fs.name] = len(index)
ss.m[fs.name] = fs
ss.l = append(ss.l, fs)
}
}
}
}
var (
structSpecMutex sync.RWMutex
structSpecCache = make(map[reflect.Type]*structSpec)
defaultFieldSpec = &fieldSpec{}
)
func structSpecForType(t reflect.Type) *structSpec {
structSpecMutex.RLock()
ss, found := structSpecCache[t]
structSpecMutex.RUnlock()
if found {
return ss
}
structSpecMutex.Lock()
defer structSpecMutex.Unlock()
ss, found = structSpecCache[t]
if found {
return ss
}
ss = &structSpec{m: make(map[string]*fieldSpec)}
compileStructSpec(t, make(map[string]int), nil, ss)
structSpecCache[t] = ss
return ss
}
var errScanStructValue = errors.New("redigo.ScanStruct: value must be non-nil pointer to a struct")
// ScanStruct scans alternating names and values from src to a struct. The
// HGETALL and CONFIG GET commands return replies in this format.
//
// ScanStruct uses exported field names to match values in the response. Use
// 'redis' field tag to override the name:
//
// Field int `redis:"myName"`
//
// Fields with the tag redis:"-" are ignored.
//
// Each field uses RedisScan if available otherwise:
// Integer, float, boolean, string and []byte fields are supported. Scan uses the
// standard strconv package to convert bulk string values to numeric and
// boolean types.
//
// If a src element is nil, then the corresponding field is not modified.
func ScanStruct(src []interface{}, dest interface{}) error {
d := reflect.ValueOf(dest)
if d.Kind() != reflect.Ptr || d.IsNil() {
return errScanStructValue
}
d = d.Elem()
if d.Kind() != reflect.Struct {
return errScanStructValue
}
ss := structSpecForType(d.Type())
if len(src)%2 != 0 {
return errors.New("redigo.ScanStruct: number of values not a multiple of 2")
}
for i := 0; i < len(src); i += 2 {
s := src[i+1]
if s == nil {
continue
}
name, ok := src[i].([]byte)
if !ok {
return fmt.Errorf("redigo.ScanStruct: key %d not a bulk string value", i)
}
fs := ss.fieldSpec(name)
if fs == nil {
continue
}
if err := convertAssignValue(d.FieldByIndex(fs.index), s); err != nil {
return fmt.Errorf("redigo.ScanStruct: cannot assign field %s: %v", fs.name, err)
}
}
return nil
}
var (
errScanSliceValue = errors.New("redigo.ScanSlice: dest must be non-nil pointer to a struct")
)
// ScanSlice scans src to the slice pointed to by dest. The elements the dest
// slice must be integer, float, boolean, string, struct or pointer to struct
// values.
//
// Struct fields must be integer, float, boolean or string values. All struct
// fields are used unless a subset is specified using fieldNames.
func ScanSlice(src []interface{}, dest interface{}, fieldNames ...string) error {
d := reflect.ValueOf(dest)
if d.Kind() != reflect.Ptr || d.IsNil() {
return errScanSliceValue
}
d = d.Elem()
if d.Kind() != reflect.Slice {
return errScanSliceValue
}
isPtr := false
t := d.Type().Elem()
if t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct {
isPtr = true
t = t.Elem()
}
if t.Kind() != reflect.Struct {
ensureLen(d, len(src))
for i, s := range src {
if s == nil {
continue
}
if err := convertAssignValue(d.Index(i), s); err != nil {
return fmt.Errorf("redigo.ScanSlice: cannot assign element %d: %v", i, err)
}
}
return nil
}
ss := structSpecForType(t)
fss := ss.l
if len(fieldNames) > 0 {
fss = make([]*fieldSpec, len(fieldNames))
for i, name := range fieldNames {
fss[i] = ss.m[name]
if fss[i] == nil {
return fmt.Errorf("redigo.ScanSlice: ScanSlice bad field name %s", name)
}
}
}
if len(fss) == 0 {
return errors.New("redigo.ScanSlice: no struct fields")
}
n := len(src) / len(fss)
if n*len(fss) != len(src) {
return errors.New("redigo.ScanSlice: length not a multiple of struct field count")
}
ensureLen(d, n)
for i := 0; i < n; i++ {
d := d.Index(i)
if isPtr {
if d.IsNil() {
d.Set(reflect.New(t))
}
d = d.Elem()
}
for j, fs := range fss {
s := src[i*len(fss)+j]
if s == nil {
continue
}
if err := convertAssignValue(d.FieldByIndex(fs.index), s); err != nil {
return fmt.Errorf("redigo.ScanSlice: cannot assign element %d to field %s: %v", i*len(fss)+j, fs.name, err)
}
}
}
return nil
}
// Args is a helper for constructing command arguments from structured values.
type Args []interface{}
// Add returns the result of appending value to args.
func (args Args) Add(value ...interface{}) Args {
return append(args, value...)
}
// AddFlat returns the result of appending the flattened value of v to args.
//
// Maps are flattened by appending the alternating keys and map values to args.
//
// Slices are flattened by appending the slice elements to args.
//
// Structs are flattened by appending the alternating names and values of
// exported fields to args. If v is a nil struct pointer, then nothing is
// appended. The 'redis' field tag overrides struct field names. See ScanStruct
// for more information on the use of the 'redis' field tag.
//
// Other types are appended to args as is.
func (args Args) AddFlat(v interface{}) Args {
rv := reflect.ValueOf(v)
switch rv.Kind() {
case reflect.Struct:
args = flattenStruct(args, rv)
case reflect.Slice:
for i := 0; i < rv.Len(); i++ {
args = append(args, rv.Index(i).Interface())
}
case reflect.Map:
for _, k := range rv.MapKeys() {
args = append(args, k.Interface(), rv.MapIndex(k).Interface())
}
case reflect.Ptr:
if rv.Type().Elem().Kind() == reflect.Struct {
if !rv.IsNil() {
args = flattenStruct(args, rv.Elem())
}
} else {
args = append(args, v)
}
default:
args = append(args, v)
}
return args
}
func flattenStruct(args Args, v reflect.Value) Args {
ss := structSpecForType(v.Type())
for _, fs := range ss.l {
fv := v.FieldByIndex(fs.index)
if fs.omitEmpty {
var empty = false
switch fv.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
empty = fv.Len() == 0
case reflect.Bool:
empty = !fv.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
empty = fv.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
empty = fv.Uint() == 0
case reflect.Float32, reflect.Float64:
empty = fv.Float() == 0
case reflect.Interface, reflect.Ptr:
empty = fv.IsNil()
}
if empty {
continue
}
}
args = append(args, fs.name, fv.Interface())
}
return args
}

View File

@ -1,91 +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 (
"crypto/sha1"
"encoding/hex"
"io"
"strings"
)
// Script encapsulates the source, hash and key count for a Lua script. See
// http://redis.io/commands/eval for information on scripts in Redis.
type Script struct {
keyCount int
src string
hash string
}
// NewScript returns a new script object. If keyCount is greater than or equal
// to zero, then the count is automatically inserted in the EVAL command
// argument list. If keyCount is less than zero, then the application supplies
// the count as the first value in the keysAndArgs argument to the Do, Send and
// SendHash methods.
func NewScript(keyCount int, src string) *Script {
h := sha1.New()
io.WriteString(h, src)
return &Script{keyCount, src, hex.EncodeToString(h.Sum(nil))}
}
func (s *Script) args(spec string, keysAndArgs []interface{}) []interface{} {
var args []interface{}
if s.keyCount < 0 {
args = make([]interface{}, 1+len(keysAndArgs))
args[0] = spec
copy(args[1:], keysAndArgs)
} else {
args = make([]interface{}, 2+len(keysAndArgs))
args[0] = spec
args[1] = s.keyCount
copy(args[2:], keysAndArgs)
}
return args
}
// Hash returns the script hash.
func (s *Script) Hash() string {
return s.hash
}
// Do evaluates the script. Under the covers, Do optimistically evaluates the
// script using the EVALSHA command. If the command fails because the script is
// not loaded, then Do evaluates the script using the EVAL command (thus
// causing the script to load).
func (s *Script) Do(c Conn, keysAndArgs ...interface{}) (interface{}, error) {
v, err := c.Do("EVALSHA", s.args(s.hash, keysAndArgs)...)
if e, ok := err.(Error); ok && strings.HasPrefix(string(e), "NOSCRIPT ") {
v, err = c.Do("EVAL", s.args(s.src, keysAndArgs)...)
}
return v, err
}
// SendHash evaluates the script without waiting for the reply. The script is
// evaluated with the EVALSHA command. The application must ensure that the
// script is loaded by a previous call to Send, Do or Load methods.
func (s *Script) SendHash(c Conn, keysAndArgs ...interface{}) error {
return c.Send("EVALSHA", s.args(s.hash, keysAndArgs)...)
}
// Send evaluates the script without waiting for the reply.
func (s *Script) Send(c Conn, keysAndArgs ...interface{}) error {
return c.Send("EVAL", s.args(s.src, keysAndArgs)...)
}
// Load loads the script without evaluating it.
func (s *Script) Load(c Conn) error {
_, err := c.Do("SCRIPT", "LOAD", s.src)
return err
}

5
vendor/github.com/go-ini/ini/.gitignore generated vendored Normal file
View File

@ -0,0 +1,5 @@
testdata/conf_out.ini
ini.sublime-project
ini.sublime-workspace
testdata/conf_reflect.ini
.idea

14
vendor/github.com/go-ini/ini/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,14 @@
sudo: false
language: go
go:
- 1.4.x
- 1.5.x
- 1.6.x
- 1.7.x
- 1.8.x
- master
script:
- go get golang.org/x/tools/cmd/cover
- go get github.com/smartystreets/goconvey
- go test -v -cover -race

9
vendor/github.com/go-sql-driver/mysql/.gitignore generated vendored Normal file
View File

@ -0,0 +1,9 @@
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
Icon?
ehthumbs.db
Thumbs.db
.idea

93
vendor/github.com/go-sql-driver/mysql/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,93 @@
sudo: false
language: go
go:
- 1.5
- 1.6
- 1.7
- 1.8
- 1.9
- tip
before_install:
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
before_script:
- echo -e "[server]\ninnodb_log_file_size=256MB\ninnodb_buffer_pool_size=512MB\nmax_allowed_packet=16MB" | sudo tee -a /etc/mysql/my.cnf
- sudo service mysql restart
- .travis/wait_mysql.sh
- mysql -e 'create database gotest;'
matrix:
include:
- env: DB=MYSQL57
sudo: required
dist: trusty
go: 1.9
services:
- docker
before_install:
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
- docker pull mysql:5.7
- docker run -d -p 127.0.0.1:3307:3306 --name mysqld -e MYSQL_DATABASE=gotest -e MYSQL_USER=gotest -e MYSQL_PASSWORD=secret -e MYSQL_ROOT_PASSWORD=verysecret
mysql:5.7 --innodb_log_file_size=256MB --innodb_buffer_pool_size=512MB --max_allowed_packet=16MB
- sleep 30
- cp .travis/docker.cnf ~/.my.cnf
- mysql --print-defaults
- .travis/wait_mysql.sh
before_script:
- export MYSQL_TEST_USER=gotest
- export MYSQL_TEST_PASS=secret
- export MYSQL_TEST_ADDR=127.0.0.1:3307
- export MYSQL_TEST_CONCURRENT=1
- env: DB=MARIA55
sudo: required
dist: trusty
go: 1.9
services:
- docker
before_install:
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
- docker pull mariadb:5.5
- docker run -d -p 127.0.0.1:3307:3306 --name mysqld -e MYSQL_DATABASE=gotest -e MYSQL_USER=gotest -e MYSQL_PASSWORD=secret -e MYSQL_ROOT_PASSWORD=verysecret
mariadb:5.5 --innodb_log_file_size=256MB --innodb_buffer_pool_size=512MB --max_allowed_packet=16MB
- sleep 30
- cp .travis/docker.cnf ~/.my.cnf
- mysql --print-defaults
- .travis/wait_mysql.sh
before_script:
- export MYSQL_TEST_USER=gotest
- export MYSQL_TEST_PASS=secret
- export MYSQL_TEST_ADDR=127.0.0.1:3307
- export MYSQL_TEST_CONCURRENT=1
- env: DB=MARIA10_1
sudo: required
dist: trusty
go: 1.9
services:
- docker
before_install:
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
- docker pull mariadb:10.1
- docker run -d -p 127.0.0.1:3307:3306 --name mysqld -e MYSQL_DATABASE=gotest -e MYSQL_USER=gotest -e MYSQL_PASSWORD=secret -e MYSQL_ROOT_PASSWORD=verysecret
mariadb:10.1 --innodb_log_file_size=256MB --innodb_buffer_pool_size=512MB --max_allowed_packet=16MB
- sleep 30
- cp .travis/docker.cnf ~/.my.cnf
- mysql --print-defaults
- .travis/wait_mysql.sh
before_script:
- export MYSQL_TEST_USER=gotest
- export MYSQL_TEST_PASS=secret
- export MYSQL_TEST_ADDR=127.0.0.1:3307
- export MYSQL_TEST_CONCURRENT=1
script:
- go test -v -covermode=count -coverprofile=coverage.out
- go vet ./...
- test -z "$(gofmt -d -s . | tee /dev/stderr)"
- $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci

1
vendor/github.com/go-xorm/core/.gitignore generated vendored Normal file
View File

@ -0,0 +1 @@
*.db

0
vendor/github.com/go-xorm/core/benchmark.sh generated vendored Executable file → Normal file
View File

View File

@ -1,28 +0,0 @@
Copyright (c) 2014, go-xorm
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the {organization} nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,9 +0,0 @@
xorm-redis-cache
================
XORM Redis Cache
[![Go Walker](http://gowalker.org/api/v1/badge)](http://gowalker.org/github.com/go-xorm/xorm-redis-cache)

View File

@ -1,302 +0,0 @@
package xormrediscache
import (
"bytes"
"encoding/gob"
"fmt"
"github.com/garyburd/redigo/redis"
"github.com/go-xorm/core"
"hash/crc32"
// "log"
"reflect"
// "strconv"
"time"
"unsafe"
)
const (
DEFAULT_EXPIRATION = time.Duration(0)
FOREVER_EXPIRATION = time.Duration(-1)
LOGGING_PREFIX = "[redis_cacher]"
)
// Wraps the Redis client to meet the Cache interface.
type RedisCacher struct {
pool *redis.Pool
defaultExpiration time.Duration
Logger core.ILogger
}
// New a Redis Cacher, host as IP endpoint, i.e., localhost:6379, provide empty string or nil if Redis server doesn't
// require AUTH command, defaultExpiration sets the expire duration for a key to live. Until redigo supports
// sharding/clustering, only one host will be in hostList
//
// engine.SetDefaultCacher(xormrediscache.NewRedisCacher("localhost:6379", "", xormrediscache.DEFAULT_EXPIRATION, engine.Logger))
//
// or set MapCacher
//
// engine.MapCacher(&user, xormrediscache.NewRedisCacher("localhost:6379", "", xormrediscache.DEFAULT_EXPIRATION, engine.Logger))
//
func NewRedisCacher(host string, password string, defaultExpiration time.Duration, logger core.ILogger) *RedisCacher {
var pool = &redis.Pool{
MaxIdle: 5,
IdleTimeout: 240 * time.Second,
Dial: func() (redis.Conn, error) {
// the redis protocol should probably be made sett-able
c, err := redis.Dial("tcp", host)
if err != nil {
return nil, err
}
if len(password) > 0 {
if _, err := c.Do("AUTH", password); err != nil {
c.Close()
return nil, err
}
} else {
// check with PING
if _, err := c.Do("PING"); err != nil {
c.Close()
return nil, err
}
}
return c, err
},
// custom connection test method
TestOnBorrow: func(c redis.Conn, t time.Time) error {
if _, err := c.Do("PING"); err != nil {
return err
}
return nil
},
}
return &RedisCacher{pool: pool, defaultExpiration: defaultExpiration, Logger: logger}
}
func exists(conn redis.Conn, key string) bool {
existed, _ := redis.Bool(conn.Do("EXISTS", key))
return existed
}
func (c *RedisCacher) logErrf(format string, contents ...interface{}) {
if c.Logger != nil {
c.Logger.Errorf(fmt.Sprintf("%s %s", LOGGING_PREFIX, format), contents...)
}
}
func (c *RedisCacher) logDebugf(format string, contents ...interface{}) {
if c.Logger != nil {
c.Logger.Debugf(fmt.Sprintf("%s %s", LOGGING_PREFIX, format), contents...)
}
}
func (c *RedisCacher) getBeanKey(tableName string, id string) string {
return fmt.Sprintf("xorm:bean:%s:%s", tableName, id)
}
func (c *RedisCacher) getSqlKey(tableName string, sql string) string {
// hash sql to minimize key length
crc := crc32.ChecksumIEEE([]byte(sql))
return fmt.Sprintf("xorm:sql:%s:%d", tableName, crc)
}
// Delete all xorm cached objects
func (c *RedisCacher) Flush() error {
// conn := c.pool.Get()
// defer conn.Close()
// _, err := conn.Do("FLUSHALL")
// return err
return c.delObject("xorm:*")
}
func (c *RedisCacher) getObject(key string) interface{} {
conn := c.pool.Get()
defer conn.Close()
raw, err := conn.Do("GET", key)
if raw == nil {
return nil
}
item, err := redis.Bytes(raw, err)
if err != nil {
c.logErrf("redis.Bytes failed: %s", err)
return nil
}
value, err := c.deserialize(item)
return value
}
func (c *RedisCacher) GetIds(tableName, sql string) interface{} {
sqlKey := c.getSqlKey(tableName, sql)
c.logDebugf(" GetIds|tableName:%s|sql:%s|key:%s", tableName, sql, sqlKey)
return c.getObject(sqlKey)
}
func (c *RedisCacher) GetBean(tableName string, id string) interface{} {
beanKey := c.getBeanKey(tableName, id)
c.logDebugf("[xorm/redis_cacher] GetBean|tableName:%s|id:%s|key:%s", tableName, id, beanKey)
return c.getObject(beanKey)
}
func (c *RedisCacher) putObject(key string, value interface{}) {
c.invoke(c.pool.Get().Do, key, value, c.defaultExpiration)
}
func (c *RedisCacher) PutIds(tableName, sql string, ids interface{}) {
sqlKey := c.getSqlKey(tableName, sql)
c.logDebugf("PutIds|tableName:%s|sql:%s|key:%s|obj:%s|type:%v", tableName, sql, sqlKey, ids, reflect.TypeOf(ids))
c.putObject(sqlKey, ids)
}
func (c *RedisCacher) PutBean(tableName string, id string, obj interface{}) {
beanKey := c.getBeanKey(tableName, id)
c.logDebugf("PutBean|tableName:%s|id:%s|key:%s|type:%v", tableName, id, beanKey, reflect.TypeOf(obj))
c.putObject(beanKey, obj)
}
func (c *RedisCacher) delObject(key string) error {
c.logDebugf("delObject key:[%s]", key)
conn := c.pool.Get()
defer conn.Close()
if !exists(conn, key) {
c.logErrf("delObject key:[%s] err: %v", key, core.ErrCacheMiss)
return core.ErrCacheMiss
}
_, err := conn.Do("DEL", key)
return err
}
func (c *RedisCacher) delObjects(key string) error {
c.logDebugf("delObjects key:[%s]", key)
conn := c.pool.Get()
defer conn.Close()
keys, err := conn.Do("KEYS", key)
c.logDebugf("delObjects keys: %v", keys)
if err == nil {
for _, key := range keys.([]interface{}) {
conn.Do("DEL", key)
}
}
return err
}
func (c *RedisCacher) DelIds(tableName, sql string) {
c.delObject(c.getSqlKey(tableName, sql))
}
func (c *RedisCacher) DelBean(tableName string, id string) {
c.delObject(c.getBeanKey(tableName, id))
}
func (c *RedisCacher) ClearIds(tableName string) {
c.delObjects(c.getSqlKey(tableName, "*"))
}
func (c *RedisCacher) ClearBeans(tableName string) {
c.delObjects(c.getBeanKey(tableName, "*"))
}
func (c *RedisCacher) invoke(f func(string, ...interface{}) (interface{}, error),
key string, value interface{}, expires time.Duration) error {
switch expires {
case DEFAULT_EXPIRATION:
expires = c.defaultExpiration
case FOREVER_EXPIRATION:
expires = time.Duration(0)
}
b, err := c.serialize(value)
if err != nil {
return err
}
conn := c.pool.Get()
defer conn.Close()
if expires > 0 {
_, err := f("SETEX", key, int32(expires/time.Second), b)
return err
} else {
_, err := f("SET", key, b)
return err
}
}
func (c *RedisCacher) serialize(value interface{}) ([]byte, error) {
err := c.registerGobConcreteType(value)
if err != nil {
return nil, err
}
if reflect.TypeOf(value).Kind() == reflect.Struct {
return nil, fmt.Errorf("serialize func only take pointer of a struct")
}
var b bytes.Buffer
encoder := gob.NewEncoder(&b)
c.logDebugf("serialize type:%v", reflect.TypeOf(value))
err = encoder.Encode(&value)
if err != nil {
c.logErrf("gob encoding '%s' failed: %s|value:%v", value, err, value)
return nil, err
}
return b.Bytes(), nil
}
func (c *RedisCacher) deserialize(byt []byte) (ptr interface{}, err error) {
b := bytes.NewBuffer(byt)
decoder := gob.NewDecoder(b)
var p interface{}
err = decoder.Decode(&p)
if err != nil {
c.logErrf("decode failed: %v", err)
return
}
v := reflect.ValueOf(p)
c.logDebugf("deserialize type:%v", v.Type())
if v.Kind() == reflect.Struct {
var pp interface{} = &p
datas := reflect.ValueOf(pp).Elem().InterfaceData()
sp := reflect.NewAt(v.Type(),
unsafe.Pointer(datas[1])).Interface()
ptr = sp
vv := reflect.ValueOf(ptr)
c.logDebugf("deserialize convert ptr type:%v | CanAddr:%t", vv.Type(), vv.CanAddr())
} else {
ptr = p
}
return
}
func (c *RedisCacher) registerGobConcreteType(value interface{}) error {
t := reflect.TypeOf(value)
c.logDebugf("registerGobConcreteType:%v", t)
switch t.Kind() {
case reflect.Ptr:
v := reflect.ValueOf(value)
i := v.Elem().Interface()
gob.Register(i)
case reflect.Struct, reflect.Map, reflect.Slice:
gob.Register(value)
case reflect.String, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Bool, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128:
// do nothing since already registered known type
default:
return fmt.Errorf("unhandled type: %v", t)
}
return nil
}

View File

@ -1,6 +0,0 @@
redis-cli FLUSHALL
if [ $? == "0" ];then
go test -v -run=TestMysqlWithCache
else
echo "no redis-server running on localhost"
fi

30
vendor/github.com/go-xorm/xorm/.gitignore generated vendored Normal file
View File

@ -0,0 +1,30 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
*.db
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.log
.vendor
temp_test.go
.vscode
xorm.test
*.sqlite3

0
vendor/github.com/go-xorm/xorm/gen_reserved.sh generated vendored Executable file → Normal file
View File

0
vendor/github.com/go-xorm/xorm/test_mssql.sh generated vendored Executable file → Normal file
View File

0
vendor/github.com/go-xorm/xorm/test_mssql_cache.sh generated vendored Executable file → Normal file
View File

0
vendor/github.com/go-xorm/xorm/test_mymysql.sh generated vendored Executable file → Normal file
View File

0
vendor/github.com/go-xorm/xorm/test_mymysql_cache.sh generated vendored Executable file → Normal file
View File

0
vendor/github.com/go-xorm/xorm/test_mysql.sh generated vendored Executable file → Normal file
View File

0
vendor/github.com/go-xorm/xorm/test_mysql_cache.sh generated vendored Executable file → Normal file
View File

0
vendor/github.com/go-xorm/xorm/test_postgres.sh generated vendored Executable file → Normal file
View File

0
vendor/github.com/go-xorm/xorm/test_postgres_cache.sh generated vendored Executable file → Normal file
View File

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