Compare commits
65 Commits
Author | SHA1 | Date |
---|---|---|
kolaente | ddc3477efd | |
konrad | 0b1212f428 | |
konrad | c097264bde | |
konrad | 2bc349a33b | |
konrad | 5673acdd60 | |
konrad | 0b4891c3ef | |
kolaente | db5e3a7cbc | |
konrad | 0030372879 | |
konrad | 3de6ecf579 | |
konrad | 91e10eb78b | |
konrad | 5fbc08cf65 | |
konrad | 0763ae07b0 | |
konrad | d553ca743b | |
konrad | 8565772ab0 | |
konrad | cfb68b9f79 | |
konrad | 7a3bc05243 | |
kolaente | 5b7fb1be87 | |
konrad | 0bf37c2ac0 | |
konrad | eebb8eb4c4 | |
konrad | d46cbc2d67 | |
konrad | 537aebc873 | |
konrad | 152a399567 | |
kolaente | ca88232109 | |
kolaente | 12d8b8f0fa | |
kolaente | 56a3215147 | |
kolaente | 989ab530dd | |
kolaente | 80f86527ed | |
kolaente | 87529b5e3e | |
kolaente | b6d0570e79 | |
konrad | e99aeb67c6 | |
konrad | f09e5c6f84 | |
konrad | 63a74ee7ac | |
konrad | aa5b510424 | |
konrad | 7ddbec7a6f | |
kolaente | ab19edfbd3 | |
konrad | 3b38e61ab8 | |
konrad | 96ad0fc31b | |
konrad | 66b6dbf4f3 | |
konrad | 70d6278540 | |
konrad | 068bfc942e | |
kolaente | a734f21ac2 | |
konrad | 3d27bb1438 | |
konrad | 815ec40696 | |
konrad | a4b8a44e47 | |
konrad | c8da860eab | |
konrad | dec5db7649 | |
konrad | c3cfc73840 | |
konrad | 2e2877156b | |
konrad | d309d5b3b6 | |
konrad | bcb8b08001 | |
konrad | 434856a44f | |
konrad | a92d711b00 | |
konrad | 4275a3acad | |
konrad | 0027b1f001 | |
konrad | 323f4c7ff4 | |
konrad | aaf5ca0ceb | |
konrad | f7314b439f | |
konrad | f27172cfd8 | |
konrad | d7fd1082a4 | |
konrad | 4b82af8ae8 | |
konrad | 772ed316cb | |
konrad | dbc3886706 | |
konrad | 6c1ecf55f9 | |
konrad | 030e2af33d | |
konrad | 24d2a6519f |
|
@ -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:
|
||||
|
|
|
@ -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*
|
6
Makefile
6
Makefile
|
@ -1,5 +1,5 @@
|
|||
DIST := dist
|
||||
IMPORT := git.mowie.cc/konrad/Library
|
||||
IMPORT := git.kolaente.de/konrad/Library
|
||||
|
||||
SED_INPLACE := sed -i
|
||||
|
||||
|
@ -21,7 +21,7 @@ EXTRA_GOFLAGS ?=
|
|||
|
||||
LDFLAGS := -X "main.Version=$(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')" -X "main.Tags=$(TAGS)"
|
||||
|
||||
PACKAGES ?= $(filter-out git.mowie.cc/konrad/Library/integrations,$(shell go list ./... | grep -v /vendor/))
|
||||
PACKAGES ?= $(filter-out git.kolaente.de/konrad/Library/integrations,$(shell go list ./... | grep -v /vendor/))
|
||||
SOURCES ?= $(shell find . -name "*.go" -type f)
|
||||
|
||||
TAGS ?=
|
||||
|
@ -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:
|
||||
|
|
|
@ -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)
|
File diff suppressed because it is too large
Load Diff
|
@ -1,3 +1 @@
|
|||
{
|
||||
"API_URL": "http://localhost:8082/api/v1/"
|
||||
}
|
||||
{"API_URL": "/api/v1/"}
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
|
|
|
@ -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>
|
|
@ -36,7 +36,9 @@ export default {
|
|||
books: 'Bücher',
|
||||
authors: 'Autoren',
|
||||
publishers: 'Verlage',
|
||||
items: 'Artikel'
|
||||
items: 'Artikel',
|
||||
users: 'Benutzer',
|
||||
logs: 'Logs'
|
||||
},
|
||||
books: {
|
||||
title: 'Bücherübersicht',
|
||||
|
@ -96,5 +98,33 @@ export default {
|
|||
errorNoTitle: 'Bitte gib mindestens einen Titel an.',
|
||||
updatedSuccess: 'Der Artikel wurde erfolgreich geupdated!',
|
||||
insertedSuccess: 'Der Artikel wurde erfolgreich erstellt!'
|
||||
},
|
||||
logs: {
|
||||
title: 'Logs',
|
||||
gridColumns: {
|
||||
user: 'Benutzer',
|
||||
action: 'Aktion',
|
||||
itemID: 'Item',
|
||||
date: 'Datum'
|
||||
},
|
||||
logActions: [
|
||||
'',
|
||||
'Buch hinzugefügt',
|
||||
'Buch geändert',
|
||||
'Buch gelöscht',
|
||||
'Author hinzugefügt',
|
||||
'Author geändert',
|
||||
'Author gelöscht',
|
||||
'Verlag hinzugefügt',
|
||||
'Verlag geändert',
|
||||
'Verlag gelöscht',
|
||||
'Artikel hinzugefügt',
|
||||
'Artikel geändert',
|
||||
'Artikel gelöscht',
|
||||
'Benutzer hinzugefügt',
|
||||
'Benutzer geändert',
|
||||
'Benutzer gelöscht',
|
||||
'Benutzerpasswort geändert'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,7 +36,9 @@ export default {
|
|||
books: 'Books',
|
||||
authors: 'Authors',
|
||||
publishers: 'Publishers',
|
||||
items: 'Items'
|
||||
items: 'Items',
|
||||
users: 'Users',
|
||||
logs: 'Logs'
|
||||
},
|
||||
books: {
|
||||
title: 'Books overview',
|
||||
|
@ -96,5 +98,33 @@ export default {
|
|||
errorNoTitle: 'Please provide at least a title.',
|
||||
updatedSuccess: 'The item was successfully updated!',
|
||||
insertedSuccess: 'The item was successfully inserted!'
|
||||
},
|
||||
logs: {
|
||||
title: 'Logs',
|
||||
gridColumns: {
|
||||
user: 'User',
|
||||
action: 'Action',
|
||||
itemID: 'Item',
|
||||
date: 'Date'
|
||||
},
|
||||
logActions: [
|
||||
'',
|
||||
'Book added',
|
||||
'Book updated',
|
||||
'Book deleted',
|
||||
'Author added',
|
||||
'Author updated',
|
||||
'Author deleted',
|
||||
'Publisher added',
|
||||
'Publisher updated',
|
||||
'Publisher deleted',
|
||||
'Item added',
|
||||
'Item updated',
|
||||
'Item deleted',
|
||||
'User added',
|
||||
'User updated',
|
||||
'User deleted',
|
||||
'Changed user password'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,7 +36,9 @@ export default {
|
|||
books: 'Livres',
|
||||
authors: 'Auteurs',
|
||||
publishers: 'Maisons d\'éditions',
|
||||
items: 'Articles'
|
||||
items: 'Articles',
|
||||
users: 'Utilisateurs',
|
||||
logs: 'Logs'
|
||||
},
|
||||
books: {
|
||||
title: 'Liste des livres',
|
||||
|
@ -98,6 +100,34 @@ export default {
|
|||
errorNoTitle: 'Veuillez au moins saisir un titre.',
|
||||
updatedSuccess: 'L\'article a éte mit a jour avec succès !',
|
||||
insertedSuccess: 'L\'article a éte crée avec succès !'
|
||||
},
|
||||
logs: {
|
||||
title: 'Logs',
|
||||
gridColumns: {
|
||||
user: 'Utilisateur',
|
||||
action: 'Action',
|
||||
itemID: 'Item',
|
||||
date: 'Date'
|
||||
},
|
||||
logActions: [
|
||||
'',
|
||||
'Livre ajouté',
|
||||
'Livre modifié',
|
||||
'Livre supprimé',
|
||||
'Auteur ajouté',
|
||||
'Auteur modifié',
|
||||
'Auteur summprimé',
|
||||
'Maison d\'edition ajoutée',
|
||||
'Maison d\'edition modifiée',
|
||||
'Maison d\'edition supprimée',
|
||||
'Article ajouté',
|
||||
'Article modifié',
|
||||
'Article supprimée',
|
||||
'Utilisateur ajouté',
|
||||
'Utilisateur modifié',
|
||||
'Utilisateur supprimé',
|
||||
'Mot de passe changé'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import PublisherOverview from '@/components/PublisherOverview'
|
|||
import Items from '@/components/Items'
|
||||
import ItemsOverview from '@/components/ItemOverview'
|
||||
import ItemsAddEdit from '@/components/ItemsAddEdit'
|
||||
import Logs from '@/components/Logs'
|
||||
|
||||
Vue.use(Router)
|
||||
|
||||
|
@ -108,6 +109,11 @@ export default new Router({
|
|||
path: '/items/:id/edit',
|
||||
name: 'item-edit',
|
||||
component: ItemsAddEdit
|
||||
},
|
||||
{
|
||||
path: '/logs',
|
||||
name: 'view-logs',
|
||||
component: Logs
|
||||
}
|
||||
]
|
||||
})
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
module git.kolaente.de/konrad/Library
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1-0.20171005155431-ecdeabc65495 // indirect
|
||||
github.com/denisenkom/go-mssqldb v0.10.0 // indirect
|
||||
github.com/dgrijalva/jwt-go v3.0.1-0.20170608005149-a539ee1a749a+incompatible
|
||||
github.com/go-ini/ini v1.28.2
|
||||
github.com/go-sql-driver/mysql v1.3.1-0.20171007150158-ee359f95877b
|
||||
github.com/go-xorm/builder v0.0.0-20170519032130-c8871c857d25 // indirect
|
||||
github.com/go-xorm/core v0.5.7
|
||||
github.com/go-xorm/xorm v0.6.4-0.20170930012613-29d4a0330a00
|
||||
github.com/joho/godotenv v1.3.0 // indirect
|
||||
github.com/labstack/echo v3.1.1-0.20170426170929-1049c9613cd3+incompatible
|
||||
github.com/labstack/gommon v0.2.2-0.20170925052817-57409ada9da0 // indirect
|
||||
github.com/lib/pq v1.10.1 // indirect
|
||||
github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd // indirect
|
||||
github.com/mattn/go-isatty v0.0.4-0.20170925054904-a5cdd64afdee // indirect
|
||||
github.com/mattn/go-oci8 v0.1.1 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.5.0
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||
github.com/stretchr/testify v1.2.1-0.20171231124224-87b1dfb5b2fa
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 // indirect
|
||||
github.com/ziutek/mymysql v1.5.4 // indirect
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/testfixtures.v2 v2.4.3
|
||||
gopkg.in/yaml.v2 v2.0.0 // indirect
|
||||
)
|
|
@ -0,0 +1,72 @@
|
|||
github.com/davecgh/go-spew v1.1.1-0.20171005155431-ecdeabc65495 h1:b2hEFhj0PgDc77eCeDUSKXynIoXJRt6yTZ8aMk2cPoI=
|
||||
github.com/davecgh/go-spew v1.1.1-0.20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisenkom/go-mssqldb v0.10.0 h1:QykgLZBorFE95+gO3u9esLd0BmbvpWp0/waNNZfHBM8=
|
||||
github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/dgrijalva/jwt-go v3.0.1-0.20170608005149-a539ee1a749a+incompatible h1:lwWnUpbS8H6DbUpe9VIY8G0vOupN8pnCPs68g9oxAJI=
|
||||
github.com/dgrijalva/jwt-go v3.0.1-0.20170608005149-a539ee1a749a+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/go-ini/ini v1.28.2 h1:drmmYv7psRpoGZkPtPKKTB+ZFSnvmwCMfNj5o1nLh2Y=
|
||||
github.com/go-ini/ini v1.28.2/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-sql-driver/mysql v1.3.1-0.20171007150158-ee359f95877b h1:U876wVumr5JIhbkg6n/bjYkgR2VwX9f/GjvHoz9tBsw=
|
||||
github.com/go-sql-driver/mysql v1.3.1-0.20171007150158-ee359f95877b/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-xorm/builder v0.0.0-20170519032130-c8871c857d25 h1:jUX9yw6+iKrs/WuysV2M6ap/ObK/07SE/a7I2uxitwM=
|
||||
github.com/go-xorm/builder v0.0.0-20170519032130-c8871c857d25/go.mod h1:M+P3wv0K2C+ynucGDEqJCeOTc+6DcAtiiqU8GrCksXY=
|
||||
github.com/go-xorm/core v0.5.7 h1:ClaJQDjHDre5Yco2MmkWKniM8NNdC/OXmoy2HfxxECw=
|
||||
github.com/go-xorm/core v0.5.7/go.mod h1:i7QESCABdFcvhgc8pdINtzlJf/6LC29if6ZJgHt9SHI=
|
||||
github.com/go-xorm/xorm v0.6.4-0.20170930012613-29d4a0330a00 h1:sryNK0GCJOjs3WNgdCMjr7AuFrF4pYf9LrQcomTg7k8=
|
||||
github.com/go-xorm/xorm v0.6.4-0.20170930012613-29d4a0330a00/go.mod h1:i7qRPD38xj/v75UV+a9pEzr5tfRaH2ndJfwt/fGbQhs=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/labstack/echo v3.1.1-0.20170426170929-1049c9613cd3+incompatible h1:mbe/VB+HbK7DFlOfYUlDOP9m28P64NFSqiHNpPu4h/Q=
|
||||
github.com/labstack/echo v3.1.1-0.20170426170929-1049c9613cd3+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
|
||||
github.com/labstack/gommon v0.2.2-0.20170925052817-57409ada9da0 h1:7AIW1qc9sYYTZLamTsRKSmVvJDXkZZrIWXHDK4Gq4X0=
|
||||
github.com/labstack/gommon v0.2.2-0.20170925052817-57409ada9da0/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
|
||||
github.com/lib/pq v1.10.1 h1:6VXZrLU0jHBYyAqrSPa+MgPfnSvTPuMgK+k0o5kVFWo=
|
||||
github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd h1:Y4ZRx+RIPFlPL4gnD/I7bdqSNXHlNop1Q6NjQuHds00=
|
||||
github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.4-0.20170925054904-a5cdd64afdee h1:L08yktFTj+MmaCAZBZKAU4EyW4hEDji2dPxLYJozx1s=
|
||||
github.com/mattn/go-isatty v0.0.4-0.20170925054904-a5cdd64afdee/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-oci8 v0.1.1 h1:aEUDxNAyDG0tv8CA3TArnDQNyc4EhnWlsfxRgDHABHM=
|
||||
github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI=
|
||||
github.com/mattn/go-sqlite3 v1.5.0 h1:cD1JkMVOQgN+75Jni3VEkSwLkElfpfS194KbtOH9jX8=
|
||||
github.com/mattn/go-sqlite3 v1.5.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/testify v1.2.1-0.20171231124224-87b1dfb5b2fa h1:umkGKiDLv+oYTelap19DADMEu2+JcsrhBnAydIELGAI=
|
||||
github.com/stretchr/testify v1.2.1-0.20171231124224-87b1dfb5b2fa/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8=
|
||||
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
|
||||
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
|
||||
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/testfixtures.v2 v2.4.3 h1:hURC7rEeqQPxZ2PUSscSYgUC5xAjd8NHOQumPxmILKo=
|
||||
gopkg.in/testfixtures.v2 v2.4.3/go.mod h1:vyAq+MYCgNpR29qitQdLZhdbLFf4mR/2MFJRFoQZZ2M=
|
||||
gopkg.in/yaml.v2 v2.0.0 h1:uUkhRGrsEyx/laRdeS6YIQKIys8pg+lRSRdVMTYjivs=
|
||||
gopkg.in/yaml.v2 v2.0.0/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
4
main.go
4
main.go
|
@ -1,8 +1,8 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"git.mowie.cc/konrad/Library/models"
|
||||
"git.mowie.cc/konrad/Library/routes"
|
||||
"git.kolaente.de/konrad/Library/models"
|
||||
"git.kolaente.de/konrad/Library/routes"
|
||||
|
||||
"context"
|
||||
"fmt"
|
||||
|
|
|
@ -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{})
|
||||
}
|
||||
|
|
|
@ -1,27 +1,37 @@
|
|||
package models
|
||||
|
||||
import "fmt"
|
||||
|
||||
// AddOrUpdateAuthor adds a new author based on an author struct
|
||||
func AddOrUpdateAuthor(author Author) (newAuthor Author, err error) {
|
||||
func AddOrUpdateAuthor(author Author, doer *User) (newAuthor Author, err error) {
|
||||
|
||||
// If the ID is 0, insert the author, otherwise update it
|
||||
if author.ID == 0 {
|
||||
// Check if the author is empty, only insert it if not
|
||||
if author.Forename == "" && author.Lastname == "" {
|
||||
return Author{}, fmt.Errorf("Author cannot be empty")
|
||||
return Author{}, ErrAuthorCannotBeEmpty{}
|
||||
}
|
||||
_, err = x.Insert(&author)
|
||||
|
||||
if err != nil {
|
||||
return Author{}, err
|
||||
}
|
||||
|
||||
// Log
|
||||
err = logAction(ActionTypeAuthorAdded, doer, author.ID)
|
||||
if err != nil {
|
||||
return Author{}, err
|
||||
}
|
||||
} else {
|
||||
_, err = x.Where("id = ?", author.ID).Update(&author)
|
||||
|
||||
if err != nil {
|
||||
return Author{}, err
|
||||
}
|
||||
|
||||
// Log
|
||||
err = logAction(ActionTypeAuthorUpdated, doer, author.ID)
|
||||
if err != nil {
|
||||
return Author{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// Get the newly inserted author
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
package models
|
||||
|
||||
import "fmt"
|
||||
|
||||
// DeleteAuthorByID deletes an author by its ID
|
||||
func DeleteAuthorByID(id int64) error {
|
||||
func DeleteAuthorByID(id int64, doer *User) error {
|
||||
// Check if the id is 0
|
||||
if id == 0 {
|
||||
return fmt.Errorf("ID cannot be 0")
|
||||
return ErrIDCannotBeZero{}
|
||||
}
|
||||
|
||||
// Delete the author
|
||||
|
@ -18,6 +16,12 @@ func DeleteAuthorByID(id int64) error {
|
|||
|
||||
// Delete all book relations associated with that author
|
||||
_, err = x.Delete(&AuthorBook{AuthorID: id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Logging
|
||||
err = logAction(ActionTypeAuthorDeleted, doer, id)
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,177 @@
|
|||
package models
|
||||
|
||||
import "fmt"
|
||||
|
||||
// =====================
|
||||
// User Operation Errors
|
||||
// =====================
|
||||
|
||||
// ErrUsernameExists represents a "UsernameAlreadyExists" kind of error.
|
||||
type ErrUsernameExists struct {
|
||||
UserID int64
|
||||
Username string
|
||||
}
|
||||
|
||||
// IsErrUsernameExists checks if an error is a ErrUsernameExists.
|
||||
func IsErrUsernameExists(err error) bool {
|
||||
_, ok := err.(ErrUsernameExists)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUsernameExists) Error() string {
|
||||
return fmt.Sprintf("a user with this username does already exist [user id: %d, username: %s]", err.UserID, err.Username)
|
||||
}
|
||||
|
||||
// ErrUserEmailExists represents a "UserEmailExists" kind of error.
|
||||
type ErrUserEmailExists struct {
|
||||
UserID int64
|
||||
Email string
|
||||
}
|
||||
|
||||
// IsErrUserEmailExists checks if an error is a ErrUserEmailExists.
|
||||
func IsErrUserEmailExists(err error) bool {
|
||||
_, ok := err.(ErrUserEmailExists)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserEmailExists) Error() string {
|
||||
return fmt.Sprintf("a user with this email does already exist [user id: %d, email: %s]", err.UserID, err.Email)
|
||||
}
|
||||
|
||||
// ErrNoUsername represents a "UsernameAlreadyExists" kind of error.
|
||||
type ErrNoUsername struct {
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// IsErrNoUsername checks if an error is a ErrUsernameExists.
|
||||
func IsErrNoUsername(err error) bool {
|
||||
_, ok := err.(ErrNoUsername)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrNoUsername) Error() string {
|
||||
return fmt.Sprintf("you need to specify a username [user id: %d]", err.UserID)
|
||||
}
|
||||
|
||||
// ErrNoUsernamePassword represents a "NoUsernamePassword" kind of error.
|
||||
type ErrNoUsernamePassword struct{}
|
||||
|
||||
// IsErrNoUsernamePassword checks if an error is a ErrNoUsernamePassword.
|
||||
func IsErrNoUsernamePassword(err error) bool {
|
||||
_, ok := err.(ErrNoUsernamePassword)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrNoUsernamePassword) Error() string {
|
||||
return fmt.Sprintf("you need to specify a username and a password")
|
||||
}
|
||||
|
||||
// ErrUserDoesNotExist represents a "UserDoesNotExist" kind of error.
|
||||
type ErrUserDoesNotExist struct {
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// IsErrUserDoesNotExist checks if an error is a ErrUserDoesNotExist.
|
||||
func IsErrUserDoesNotExist(err error) bool {
|
||||
_, ok := err.(ErrUserDoesNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserDoesNotExist) Error() string {
|
||||
return fmt.Sprintf("this user does not exist [user id: %d]", err.UserID)
|
||||
}
|
||||
|
||||
// ErrCouldNotGetUserID represents a "ErrCouldNotGetUserID" kind of error.
|
||||
type ErrCouldNotGetUserID struct{}
|
||||
|
||||
// IsErrCouldNotGetUserID checks if an error is a ErrCouldNotGetUserID.
|
||||
func IsErrCouldNotGetUserID(err error) bool {
|
||||
_, ok := err.(ErrCouldNotGetUserID)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrCouldNotGetUserID) Error() string {
|
||||
return fmt.Sprintf("could not get user ID")
|
||||
}
|
||||
|
||||
// ErrCannotDeleteLastUser represents a "ErrCannotDeleteLastUser" kind of error.
|
||||
type ErrCannotDeleteLastUser struct{}
|
||||
|
||||
// IsErrCannotDeleteLastUser checks if an error is a ErrCannotDeleteLastUser.
|
||||
func IsErrCannotDeleteLastUser(err error) bool {
|
||||
_, ok := err.(ErrCannotDeleteLastUser)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrCannotDeleteLastUser) Error() string {
|
||||
return fmt.Sprintf("cannot delete last user")
|
||||
}
|
||||
|
||||
// ===================
|
||||
// Empty things errors
|
||||
// ===================
|
||||
|
||||
// ErrIDCannotBeZero represents a "IDCannotBeZero" kind of error. Used if an ID (of something, not defined) is 0 where it should not.
|
||||
type ErrIDCannotBeZero struct{}
|
||||
|
||||
// IsErrIDCannotBeZero checks if an error is a ErrIDCannotBeZero.
|
||||
func IsErrIDCannotBeZero(err error) bool {
|
||||
_, ok := err.(ErrIDCannotBeZero)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrIDCannotBeZero) Error() string {
|
||||
return fmt.Sprintf("ID cannot be 0")
|
||||
}
|
||||
|
||||
// ErrAuthorCannotBeEmpty represents a "AuthorCannotBeEmpty" kind of error.
|
||||
type ErrAuthorCannotBeEmpty struct{}
|
||||
|
||||
// IsErrAuthorCannotBeEmpty checks if an error is a ErrAuthorCannotBeEmpty.
|
||||
func IsErrAuthorCannotBeEmpty(err error) bool {
|
||||
_, ok := err.(ErrAuthorCannotBeEmpty)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrAuthorCannotBeEmpty) Error() string {
|
||||
return fmt.Sprintf("author cannot be empty")
|
||||
}
|
||||
|
||||
// ErrItemTitleCannotBeEmpty represents a "ErrItemTitleCannotBeEmpty" kind of error.
|
||||
type ErrItemTitleCannotBeEmpty struct{}
|
||||
|
||||
// IsErrItemTitleCannotBeEmpty checks if an error is a ErrItemTitleCannotBeEmpty.
|
||||
func IsErrItemTitleCannotBeEmpty(err error) bool {
|
||||
_, ok := err.(ErrItemTitleCannotBeEmpty)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrItemTitleCannotBeEmpty) Error() string {
|
||||
return fmt.Sprintf("title cannot be empty")
|
||||
}
|
||||
|
||||
// ErrBookTitleCannotBeEmpty represents a "ErrBookTitleCannotBeEmpty" kind of error.
|
||||
type ErrBookTitleCannotBeEmpty struct{}
|
||||
|
||||
// IsErrBookTitleCannotBeEmpty checks if an error is a ErrBookTitleCannotBeEmpty.
|
||||
func IsErrBookTitleCannotBeEmpty(err error) bool {
|
||||
_, ok := err.(ErrBookTitleCannotBeEmpty)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrBookTitleCannotBeEmpty) Error() string {
|
||||
return fmt.Sprintf("the book should at least have a title")
|
||||
}
|
||||
|
||||
// ErrNoPublisherName represents a "ErrNoPublisherName" kind of error.
|
||||
type ErrNoPublisherName struct{}
|
||||
|
||||
// IsErrNoPublisherName checks if an error is a ErrNoPublisherName.
|
||||
func IsErrNoPublisherName(err error) bool {
|
||||
_, ok := err.(ErrNoPublisherName)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrNoPublisherName) Error() string {
|
||||
return fmt.Sprintf("you need at least a name to insert a new publisher")
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
-
|
||||
id: 1
|
||||
name: 'John Doe'
|
||||
username: 'user1'
|
||||
password: '1234'
|
||||
email: 'johndoe@example.com'
|
||||
is_admin: true
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
package models
|
||||
|
||||
import "fmt"
|
||||
|
||||
// DeleteItemByID deletes a item by its ID
|
||||
func DeleteItemByID(id int64) error {
|
||||
func DeleteItemByID(id int64, doer *User) error {
|
||||
// Check if the id is 0
|
||||
if id == 0 {
|
||||
return fmt.Errorf("ID cannot be 0")
|
||||
return ErrIDCannotBeZero{}
|
||||
}
|
||||
|
||||
// Delete the item
|
||||
|
@ -18,6 +16,12 @@ func DeleteItemByID(id int64) error {
|
|||
|
||||
// Delete all quantites for this item
|
||||
_, err = x.Delete(&Quantity{ItemID: id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Logging
|
||||
err = logAction(ActionTypeItemDeleted, doer, id)
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package models
|
||||
|
||||
// ActionType is the action type
|
||||
type ActionType int
|
||||
|
||||
// Define action types
|
||||
const (
|
||||
ActionTypeUnknown ActionType = -1
|
||||
ActionTypeBookAdded ActionType = iota
|
||||
ActionTypeBookUpdated
|
||||
ActionTypeBookDeleted
|
||||
ActionTypeAuthorAdded
|
||||
ActionTypeAuthorUpdated
|
||||
ActionTypeAuthorDeleted
|
||||
ActionTypePublisherAdded
|
||||
ActionTypePublisherUpdated
|
||||
ActionTypePublisherDeleted
|
||||
ActionTypeItemAdded
|
||||
ActionTypeItemUpdated
|
||||
ActionTypeItemDeleted
|
||||
ActionTypeUserAdded
|
||||
ActionTypeUserUpdated
|
||||
ActionTypeUserDeleted
|
||||
ActionTypeChangedUserPassword
|
||||
)
|
||||
|
||||
// LogAction logs a user action
|
||||
func logAction(actionType ActionType, user *User, itemID int64) (err error) {
|
||||
_, err = x.Insert(UserLog{Log: actionType, UserID: user.ID, ItemID: itemID})
|
||||
return
|
||||
}
|
||||
|
||||
// GetAllLogs returns an array with all logs
|
||||
func GetAllLogs() (logs []UserLog, err error) {
|
||||
err = x.OrderBy("id DESC").Find(&logs)
|
||||
if err != nil {
|
||||
return logs, err
|
||||
}
|
||||
|
||||
return logs, err
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
105
models/user.go
105
models/user.go
|
@ -1,7 +1,6 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/labstack/echo"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
@ -15,22 +14,23 @@ type UserLogin struct {
|
|||
|
||||
// User holds information about an user
|
||||
type User struct {
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk"`
|
||||
Name string `xorm:"varchar(250)"`
|
||||
Username string `xorm:"varchar(250) not null unique"`
|
||||
Password string `xorm:"varchar(250) not null"`
|
||||
Email string `xorm:"varchar(250)"`
|
||||
Created int64 `xorm:"created"`
|
||||
Updated int64 `xorm:"updated"`
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id"`
|
||||
Name string `xorm:"varchar(250)" json:"name"`
|
||||
Username string `xorm:"varchar(250) not null unique" json:"username"`
|
||||
Password string `xorm:"varchar(250) not null" json:"password"`
|
||||
Email string `xorm:"varchar(250)" json:"email"`
|
||||
IsAdmin bool `xorm:"tinyint(1) not null" json:"isAdmin"`
|
||||
Created int64 `xorm:"created" json:"created"`
|
||||
Updated int64 `xorm:"updated" json:"updated"`
|
||||
}
|
||||
|
||||
// UserLog logs user actions
|
||||
type UserLog struct {
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk"`
|
||||
UserID int64 `xorm:"int(11)"`
|
||||
Log string `xorm:"varchar(250)"`
|
||||
ItemID int64 `xorm:"int(11)"`
|
||||
Time int64 `xorm:"created"`
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id"`
|
||||
UserID int64 `xorm:"int(11)" json:"userID"`
|
||||
Log ActionType `xorm:"int(11)" json:"log"`
|
||||
ItemID int64 `xorm:"int(11)" json:"itemID"`
|
||||
Time int64 `xorm:"created" json:"time"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for users
|
||||
|
@ -40,6 +40,11 @@ func (User) TableName() string {
|
|||
|
||||
// GetUserByID gets informations about a user by its ID
|
||||
func GetUserByID(id int64) (user User, exists bool, err error) {
|
||||
// Apparently xorm does otherwise look for all users but return only one, which leads to returing one even if the ID is 0
|
||||
if id == 0 {
|
||||
return User{}, false, nil
|
||||
}
|
||||
|
||||
return GetUser(User{ID: id})
|
||||
}
|
||||
|
||||
|
@ -47,61 +52,21 @@ func GetUserByID(id int64) (user User, exists bool, err error) {
|
|||
func GetUser(user User) (userOut User, exists bool, err error) {
|
||||
userOut = user
|
||||
exists, err = x.Get(&userOut)
|
||||
//fmt.Println(user, userOut, exists, err)
|
||||
return userOut, exists, err
|
||||
}
|
||||
|
||||
// CreateUser creates a new user and inserts it into the database
|
||||
func CreateUser(user User) (newUser User, err error) {
|
||||
|
||||
newUser = user
|
||||
|
||||
// Check if we have all needed informations
|
||||
if newUser.Password == "" || newUser.Username == "" {
|
||||
return User{}, fmt.Errorf("you need to specify at least a username and a password")
|
||||
}
|
||||
|
||||
// Check if the user already existst
|
||||
_, exists, err := GetUser(User{Name: newUser.Name})
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
if exists {
|
||||
return User{}, fmt.Errorf("this username is already taken. Please use another")
|
||||
}
|
||||
|
||||
// Hash the password
|
||||
newUser.Password, err = hashPassword(user.Password)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
// Insert it
|
||||
_, err = x.Insert(newUser)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
return newUser, nil
|
||||
}
|
||||
|
||||
// HashPassword hashes a password
|
||||
func hashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
// CheckUserCredentials checks user credentials
|
||||
func CheckUserCredentials(u *UserLogin) (User, error) {
|
||||
|
||||
// Check if the user exists
|
||||
var user = User{Username: u.Username}
|
||||
exists, err := x.Get(&user)
|
||||
user, exists, err := GetUser(User{Username: u.Username})
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return User{}, fmt.Errorf("user does not exist")
|
||||
return User{}, ErrUserDoesNotExist{}
|
||||
}
|
||||
|
||||
// Check the users password
|
||||
|
@ -120,7 +85,7 @@ func GetCurrentUser(c echo.Context) (user User, err error) {
|
|||
claims := jwtinf.Claims.(jwt.MapClaims)
|
||||
userID, ok := claims["id"].(float64)
|
||||
if !ok {
|
||||
return user, fmt.Errorf("Error getting UserID")
|
||||
return user, ErrCouldNotGetUserID{}
|
||||
}
|
||||
user = User{
|
||||
ID: int64(userID),
|
||||
|
@ -133,18 +98,28 @@ func GetCurrentUser(c echo.Context) (user User, err error) {
|
|||
}
|
||||
|
||||
// LogAction logs a user action
|
||||
func logAction(action string, user User, itemID int64) (err error) {
|
||||
_, err = x.Insert(UserLog{Log: action, UserID: user.ID, ItemID: itemID})
|
||||
return
|
||||
}
|
||||
|
||||
// LogAction logs a user action
|
||||
func LogAction(action string, itemID int64, c echo.Context) (err error) {
|
||||
func LogAction(actionType ActionType, itemID int64, c echo.Context) (err error) {
|
||||
// Get the user options
|
||||
user, err := GetCurrentUser(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return logAction(action, user, itemID)
|
||||
return logAction(actionType, &user, itemID)
|
||||
}
|
||||
|
||||
// IsAdmin checks based on it's JWT token if the user is admin
|
||||
func IsAdmin(c echo.Context) bool {
|
||||
|
||||
// Get the users JWT token
|
||||
jwtinf := c.Get("user").(*jwt.Token)
|
||||
claims := jwtinf.Claims.(jwt.MapClaims)
|
||||
|
||||
// And check if he is admin
|
||||
if claims["admin"].(bool) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Send him to nirvarna if not
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// CreateUser creates a new user and inserts it into the database
|
||||
func CreateUser(user User, doer *User) (newUser User, err error) {
|
||||
|
||||
newUser = user
|
||||
|
||||
// Check if we have all needed informations
|
||||
if newUser.Password == "" || newUser.Username == "" {
|
||||
return User{}, ErrNoUsernamePassword{}
|
||||
}
|
||||
|
||||
// Check if the user already existst with that username
|
||||
existingUser, exists, err := GetUser(User{Username: newUser.Username})
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
if exists {
|
||||
return User{}, ErrUsernameExists{existingUser.ID, existingUser.Username}
|
||||
}
|
||||
|
||||
// Check if the user already existst with that username
|
||||
existingUser, exists, err = GetUser(User{Email: newUser.Email})
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
if exists {
|
||||
return User{}, ErrUserEmailExists{existingUser.ID, existingUser.Email}
|
||||
}
|
||||
|
||||
// Hash the password
|
||||
newUser.Password, err = hashPassword(user.Password)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
// Insert it
|
||||
_, err = x.Insert(newUser)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
// Get the full new User
|
||||
newUserOut, _, err := GetUser(newUser)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
// Logging
|
||||
err = logAction(ActionTypeUserAdded, doer, newUser.ID)
|
||||
|
||||
return newUserOut, err
|
||||
}
|
||||
|
||||
// HashPassword hashes a password
|
||||
func hashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
// UpdateUser updates a user
|
||||
func UpdateUser(user User, doer *User) (updatedUser User, err error) {
|
||||
|
||||
// Check if it exists
|
||||
theUser, exists, err := GetUserByID(user.ID)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
if exists {
|
||||
// Check if we have at least a username
|
||||
if user.Username == "" {
|
||||
//return User{}, ErrNoUsername{user.ID}
|
||||
user.Username = theUser.Username // Dont change the username if we dont have one
|
||||
}
|
||||
|
||||
user.Password = theUser.Password // set the password to the one in the database to not accedently resetting it
|
||||
|
||||
// Update it
|
||||
_, err = x.Id(user.ID).Update(user)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
// Get the newly updated user
|
||||
updatedUser, _, err = GetUserByID(user.ID)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
// Logging
|
||||
err = logAction(ActionTypeUserUpdated, doer, user.ID)
|
||||
|
||||
return updatedUser, err
|
||||
}
|
||||
|
||||
return User{}, ErrUserDoesNotExist{user.ID}
|
||||
}
|
||||
|
||||
// UpdateUserPassword updates the password of a user
|
||||
func UpdateUserPassword(userID int64, newPassword string, doer *User) (err error) {
|
||||
|
||||
// Get all user details
|
||||
user, exists, err := GetUserByID(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return ErrUserDoesNotExist{userID}
|
||||
}
|
||||
|
||||
// Hash the new password and set it
|
||||
hashed, err := hashPassword(newPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.Password = hashed
|
||||
|
||||
// Update it
|
||||
_, err = x.Id(user.ID).Update(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Logging
|
||||
err = logAction(ActionTypeChangedUserPassword, doer, user.ID)
|
||||
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package models
|
||||
|
||||
// DeleteUserByID deletes a user by its ID
|
||||
func DeleteUserByID(id int64, doer *User) error {
|
||||
// Check if the id is 0
|
||||
if id == 0 {
|
||||
return ErrIDCannotBeZero{}
|
||||
}
|
||||
|
||||
// Check if there is > 1 user
|
||||
total, err := x.Count(User{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if total < 2 {
|
||||
return ErrCannotDeleteLastUser{}
|
||||
}
|
||||
|
||||
// Delete the user
|
||||
_, err = x.Id(id).Delete(&User{})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Logging
|
||||
err = logAction(ActionTypeUserDeleted, doer, id)
|
||||
|
||||
return err
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
package models
|
||||
|
||||
// ListUsers returns a list with all users, filtered by an optional searchstring
|
||||
func ListUsers(searchterm string) (users []User, err error) {
|
||||
|
||||
if searchterm == "" {
|
||||
err = x.Find(&users)
|
||||
} else {
|
||||
err = x.
|
||||
Where("username LIKE ?", "%"+searchterm+"%").
|
||||
Or("name LIKE ?", "%"+searchterm+"%").
|
||||
Find(&users)
|
||||
}
|
||||
|
||||
// Obfuscate the password. Selecting everything except the password didn't work.
|
||||
for i := range users {
|
||||
users[i].Password = ""
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return []User{}, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"git.mowie.cc/konrad/Library/models"
|
||||
"git.kolaente.de/konrad/Library/models"
|
||||
"github.com/labstack/echo"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
@ -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"})
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"git.mowie.cc/konrad/Library/models"
|
||||
"git.kolaente.de/konrad/Library/models"
|
||||
"github.com/labstack/echo"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
@ -18,7 +18,7 @@ func AuthorShow(c echo.Context) error {
|
|||
// Make int
|
||||
authorID, err := strconv.ParseInt(author, 10, 64)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, models.Message{"Error getting author infos."})
|
||||
return c.JSON(http.StatusBadRequest, models.Message{"Author ID is invalid."})
|
||||
}
|
||||
|
||||
// Get Author Infos
|
||||
|
|
|
@ -2,7 +2,7 @@ package v1
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"git.mowie.cc/konrad/Library/models"
|
||||
"git.kolaente.de/konrad/Library/models"
|
||||
"github.com/labstack/echo"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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"})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"})
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"git.mowie.cc/konrad/Library/models"
|
||||
"git.kolaente.de/konrad/Library/models"
|
||||
"github.com/labstack/echo"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
@ -18,7 +18,7 @@ func ItemShow(c echo.Context) error {
|
|||
// Make int
|
||||
itemID, err := strconv.ParseInt(item, 10, 64)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, models.Message{"Error getting item infos."})
|
||||
return c.JSON(http.StatusBadRequest, models.Message{"Item ID is invalid."})
|
||||
}
|
||||
|
||||
// Get item Infos
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"git.kolaente.de/konrad/Library/models"
|
||||
"github.com/labstack/echo"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ShowLogs handels viewing logs
|
||||
func ShowLogs(c echo.Context) error {
|
||||
|
||||
// Check if the user is admin
|
||||
if !models.IsAdmin(c) {
|
||||
return echo.ErrUnauthorized
|
||||
}
|
||||
|
||||
// Get the logs
|
||||
logs, err := models.GetAllLogs()
|
||||
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, models.Message{"Error getting logs."})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, logs)
|
||||
}
|
|
@ -2,7 +2,7 @@ package v1
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"git.mowie.cc/konrad/Library/models"
|
||||
"git.kolaente.de/konrad/Library/models"
|
||||
"github.com/labstack/echo"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"})
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"git.mowie.cc/konrad/Library/models"
|
||||
"git.kolaente.de/konrad/Library/models"
|
||||
"github.com/labstack/echo"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
@ -18,7 +18,7 @@ func PublisherShow(c echo.Context) error {
|
|||
// Make int
|
||||
publisherID, err := strconv.ParseInt(publisher, 10, 64)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, models.Message{"Error getting publisher infos."})
|
||||
return c.JSON(http.StatusBadRequest, models.Message{"Publisher ID is invalid."})
|
||||
}
|
||||
|
||||
// Get Publisher Infos
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"git.mowie.cc/konrad/Library/models"
|
||||
"git.kolaente.de/konrad/Library/models"
|
||||
"github.com/labstack/echo"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
|
|
@ -2,7 +2,7 @@ package v1
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"git.mowie.cc/konrad/Library/models"
|
||||
"git.kolaente.de/konrad/Library/models"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/labstack/echo"
|
||||
)
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"git.kolaente.de/konrad/Library/models"
|
||||
"github.com/labstack/echo"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// UserAddOrUpdate is the handler to add a user
|
||||
func UserAddOrUpdate(c echo.Context) error {
|
||||
|
||||
// Check if the user is admin
|
||||
if !models.IsAdmin(c) {
|
||||
return echo.ErrUnauthorized
|
||||
}
|
||||
|
||||
// Check for Request Content
|
||||
userFromString := c.FormValue("user")
|
||||
var datUser *models.User
|
||||
|
||||
if userFromString == "" {
|
||||
// b := new(models.User)
|
||||
if err := c.Bind(&datUser); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, models.Message{"No user model provided."})
|
||||
}
|
||||
} else {
|
||||
// Decode the JSON
|
||||
dec := json.NewDecoder(strings.NewReader(userFromString))
|
||||
err := dec.Decode(&datUser)
|
||||
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, models.Message{"Error decoding user: " + err.Error()})
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have an ID other than the one in the struct
|
||||
id := c.Param("id")
|
||||
if id != "" {
|
||||
// Make int
|
||||
userID, err := strconv.ParseInt(id, 10, 64)
|
||||
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, models.Message{"Invalid ID."})
|
||||
}
|
||||
datUser.ID = userID
|
||||
}
|
||||
|
||||
// Check if the user exists
|
||||
_, exists, err := models.GetUserByID(datUser.ID)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, models.Message{"Could not check if the user exists."})
|
||||
}
|
||||
|
||||
// Get the doer options
|
||||
doer, err := models.GetCurrentUser(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Insert or update the user
|
||||
var newUser models.User
|
||||
if exists {
|
||||
newUser, err = models.UpdateUser(*datUser, &doer)
|
||||
} else {
|
||||
newUser, err = models.CreateUser(*datUser, &doer)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// Check for user already exists
|
||||
if models.IsErrUsernameExists(err) {
|
||||
return c.JSON(http.StatusBadRequest, models.Message{"A user with this username already exists."})
|
||||
}
|
||||
|
||||
// Check for user with that email already exists
|
||||
if models.IsErrUserEmailExists(err) {
|
||||
return c.JSON(http.StatusBadRequest, models.Message{"A user with this email address already exists."})
|
||||
}
|
||||
|
||||
// Check for no username provided
|
||||
if models.IsErrNoUsername(err) {
|
||||
return c.JSON(http.StatusBadRequest, models.Message{"Please specify a username."})
|
||||
}
|
||||
|
||||
// Check for no username or password provided
|
||||
if models.IsErrNoUsernamePassword(err) {
|
||||
return c.JSON(http.StatusBadRequest, models.Message{"Please specify a username and a password."})
|
||||
}
|
||||
|
||||
// Check for user does not exist
|
||||
if models.IsErrUserDoesNotExist(err) {
|
||||
return c.JSON(http.StatusBadRequest, models.Message{"The user does not exist."})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusInternalServerError, models.Message{"Error"})
|
||||
}
|
||||
|
||||
// Obfuscate his password
|
||||
newUser.Password = ""
|
||||
|
||||
return c.JSON(http.StatusOK, newUser)
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"git.kolaente.de/konrad/Library/models"
|
||||
"github.com/labstack/echo"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// UserDelete is the handler to delete a user
|
||||
func UserDelete(c echo.Context) error {
|
||||
|
||||
// Check if the user is admin
|
||||
if !models.IsAdmin(c) {
|
||||
return echo.ErrUnauthorized
|
||||
}
|
||||
|
||||
id := c.Param("id")
|
||||
|
||||
// Make int
|
||||
userID, err := strconv.ParseInt(id, 10, 64)
|
||||
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, models.Message{"User ID is invalid."})
|
||||
}
|
||||
|
||||
// Check if the user exists
|
||||
_, exists, err := models.GetUserByID(userID)
|
||||
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, models.Message{"Could not get user."})
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return c.JSON(http.StatusNotFound, models.Message{"The user does not exist."})
|
||||
}
|
||||
|
||||
// Get the doer options
|
||||
doer, err := models.GetCurrentUser(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete it
|
||||
err = models.DeleteUserByID(userID, &doer)
|
||||
|
||||
if err != nil {
|
||||
if models.IsErrIDCannotBeZero(err) {
|
||||
return c.JSON(http.StatusBadRequest, models.Message{"Id cannot be 0"})
|
||||
}
|
||||
|
||||
if models.IsErrCannotDeleteLastUser(err) {
|
||||
return c.JSON(http.StatusBadRequest, models.Message{"Cannot delete last user."})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusInternalServerError, models.Message{"Could not delete user."})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, models.Message{"success"})
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"git.kolaente.de/konrad/Library/models"
|
||||
"github.com/labstack/echo"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// UserShow gets all informations about a user
|
||||
func UserShow(c echo.Context) error {
|
||||
|
||||
// Check if the user is admin
|
||||
if !models.IsAdmin(c) {
|
||||
return echo.ErrUnauthorized
|
||||
}
|
||||
|
||||
user := c.Param("id")
|
||||
|
||||
if user == "" {
|
||||
return c.JSON(http.StatusBadRequest, models.Message{"User ID cannot be empty."})
|
||||
}
|
||||
|
||||
// Make int
|
||||
userID, err := strconv.ParseInt(user, 10, 64)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, models.Message{"User ID is invalid."})
|
||||
}
|
||||
|
||||
// Get User Infos
|
||||
userInfos, exists, err := models.GetUserByID(userID)
|
||||
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, models.Message{"Error getting user infos."})
|
||||
}
|
||||
|
||||
// Check if it exists
|
||||
if !exists {
|
||||
return c.JSON(http.StatusNotFound, models.Message{"User not found."})
|
||||
}
|
||||
|
||||
// Obfucate his password
|
||||
userInfos.Password = ""
|
||||
|
||||
return c.JSON(http.StatusOK, userInfos)
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"git.kolaente.de/konrad/Library/models"
|
||||
"github.com/labstack/echo"
|
||||
)
|
||||
|
||||
type datPassword struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// UserChangePassword is the handler to add a user
|
||||
func UserChangePassword(c echo.Context) error {
|
||||
|
||||
// Get the ID
|
||||
user := c.Param("id")
|
||||
|
||||
if user == "" {
|
||||
return c.JSON(http.StatusBadRequest, models.Message{"User ID cannot be empty."})
|
||||
}
|
||||
|
||||
// Make int
|
||||
userID, err := strconv.ParseInt(user, 10, 64)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, models.Message{"User ID is invalid."})
|
||||
}
|
||||
|
||||
// Check if the user is admin or itself
|
||||
userJWTinfo, err := models.GetCurrentUser(c)
|
||||
if !models.IsAdmin(c) {
|
||||
if userJWTinfo.ID != userID {
|
||||
return echo.ErrUnauthorized
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Request Content
|
||||
pwFromString := c.FormValue("password")
|
||||
var datPw datPassword
|
||||
|
||||
if pwFromString == "" {
|
||||
if err := c.Bind(&datPw); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, models.Message{"No password provided."})
|
||||
}
|
||||
} else {
|
||||
// Take the value directly from the input
|
||||
datPw.Password = pwFromString
|
||||
}
|
||||
|
||||
// Get User Infos
|
||||
_, exists, err := models.GetUserByID(userID)
|
||||
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, models.Message{"Error getting user infos."})
|
||||
}
|
||||
|
||||
// Check if it exists
|
||||
if !exists {
|
||||
return c.JSON(http.StatusNotFound, models.Message{"User not found."})
|
||||
}
|
||||
|
||||
// Get the doer options
|
||||
doer, err := models.GetCurrentUser(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = models.UpdateUserPassword(userID, datPw.Password, &doer)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, models.Message{"The password was updated successfully"})
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.kolaente.de/konrad/Library/models"
|
||||
"github.com/labstack/echo"
|
||||
)
|
||||
|
||||
// UsersList lists all users
|
||||
func UsersList(c echo.Context) error {
|
||||
|
||||
// Check if the user is admin
|
||||
if !models.IsAdmin(c) {
|
||||
return echo.ErrUnauthorized
|
||||
}
|
||||
|
||||
// Prepare the searchterm
|
||||
search := c.QueryParam("s")
|
||||
|
||||
list, err := models.ListUsers(search)
|
||||
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, models.Message{"Error getting users."})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, list)
|
||||
}
|
|
@ -3,7 +3,7 @@ package routes
|
|||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"git.mowie.cc/konrad/Library/models"
|
||||
"git.kolaente.de/konrad/Library/models"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/labstack/echo"
|
||||
"net/http"
|
||||
|
@ -33,6 +33,7 @@ func Login(c echo.Context) error {
|
|||
claims["username"] = user.Username
|
||||
claims["email"] = user.Email
|
||||
claims["id"] = user.ID
|
||||
claims["admin"] = user.IsAdmin
|
||||
claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
|
||||
|
||||
avatar := md5.Sum([]byte(user.Email))
|
||||
|
|
|
@ -4,8 +4,8 @@ import (
|
|||
"github.com/labstack/echo"
|
||||
"github.com/labstack/echo/middleware"
|
||||
|
||||
"git.mowie.cc/konrad/Library/models"
|
||||
apiv1 "git.mowie.cc/konrad/Library/routes/api/v1"
|
||||
"git.kolaente.de/konrad/Library/models"
|
||||
apiv1 "git.kolaente.de/konrad/Library/routes/api/v1"
|
||||
)
|
||||
|
||||
// NewEcho registers a new Echo instance
|
||||
|
@ -58,6 +58,9 @@ func RegisterRoutes(e *echo.Echo) {
|
|||
a.OPTIONS("/status/:id", SetCORSHeader)
|
||||
a.OPTIONS("/items", SetCORSHeader)
|
||||
a.OPTIONS("/items/:id", SetCORSHeader)
|
||||
a.OPTIONS("/logs", SetCORSHeader)
|
||||
a.OPTIONS("/users", SetCORSHeader)
|
||||
a.OPTIONS("/users/:id", SetCORSHeader)
|
||||
|
||||
a.POST("/login", Login)
|
||||
|
||||
|
@ -106,7 +109,18 @@ func RegisterRoutes(e *echo.Echo) {
|
|||
a.DELETE("/items/:id", apiv1.ItemDelete)
|
||||
a.POST("/items/:id", apiv1.ItemAddOrUpdate)
|
||||
|
||||
// ====== Admin Routes ======
|
||||
|
||||
// Manage Users
|
||||
a.GET("/users", apiv1.UsersList)
|
||||
a.PUT("/users", apiv1.UserAddOrUpdate)
|
||||
a.POST("/users/:id", apiv1.UserAddOrUpdate)
|
||||
a.GET("/users/:id", apiv1.UserShow)
|
||||
a.DELETE("/users/:id", apiv1.UserDelete)
|
||||
a.POST("/users/:id/password", apiv1.UserChangePassword)
|
||||
|
||||
// View logs
|
||||
a.GET("/logs", apiv1.ShowLogs)
|
||||
|
||||
/*
|
||||
Alles nur mit Api machen, davor dann einen onepager mit vue.js.
|
||||
|
@ -141,9 +155,10 @@ func RegisterRoutes(e *echo.Echo) {
|
|||
|
||||
GET /settings - |Nutzereinstellungen (Passwort, name etc)
|
||||
POST /settings - |Nutzereinstellungen (Passwort, name etc)
|
||||
GET /user - Nutzer anzeigen
|
||||
PUT /user - |neue Nutzer anlegen
|
||||
DELETE /user/:id - |nutzer löschen
|
||||
POST /user/:id - |nutzer bearbeiten
|
||||
GET /user - |Nutzer anzeigen --> Auch nur admin
|
||||
PUT /user - |neue Nutzer anlegen --> Nur admin
|
||||
DELETE /user/:id - |nutzer löschen --> Nur admins (sich selber löschen sollte nicht möglich sein)
|
||||
POST /user/:id - |nutzer bearbeiten --> Sollte entweder Admin oder der Nutzer selbst sein
|
||||
|
||||
*/
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
[Unit]
|
||||
Description=Library (A service to manage you library)
|
||||
After=syslog.target
|
||||
After=network.target
|
||||
#After=mysqld.service
|
||||
|
||||
[Service]
|
||||
RestartSec=2s
|
||||
Type=simple
|
||||
WorkingDirectory=/home/library
|
||||
ExecStart=/home/library/library
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
|
@ -0,0 +1,4 @@
|
|||
.DS_Store
|
||||
bin
|
||||
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
language: go
|
||||
|
||||
script:
|
||||
- go vet ./...
|
||||
- go test -v ./...
|
||||
|
||||
go:
|
||||
- 1.3
|
||||
- 1.4
|
||||
- 1.5
|
||||
- 1.6
|
||||
- 1.7
|
||||
- tip
|
|
@ -1,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.
|
|
@ -1,54 +0,0 @@
|
|||
// Copyright 2014 Gary Burd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package internal // import "github.com/garyburd/redigo/internal"
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
WatchState = 1 << iota
|
||||
MultiState
|
||||
SubscribeState
|
||||
MonitorState
|
||||
)
|
||||
|
||||
type CommandInfo struct {
|
||||
Set, Clear int
|
||||
}
|
||||
|
||||
var commandInfos = map[string]CommandInfo{
|
||||
"WATCH": {Set: WatchState},
|
||||
"UNWATCH": {Clear: WatchState},
|
||||
"MULTI": {Set: MultiState},
|
||||
"EXEC": {Clear: WatchState | MultiState},
|
||||
"DISCARD": {Clear: WatchState | MultiState},
|
||||
"PSUBSCRIBE": {Set: SubscribeState},
|
||||
"SUBSCRIBE": {Set: SubscribeState},
|
||||
"MONITOR": {Set: MonitorState},
|
||||
}
|
||||
|
||||
func init() {
|
||||
for n, ci := range commandInfos {
|
||||
commandInfos[strings.ToLower(n)] = ci
|
||||
}
|
||||
}
|
||||
|
||||
func LookupCommandInfo(commandName string) CommandInfo {
|
||||
if ci, ok := commandInfos[commandName]; ok {
|
||||
return ci
|
||||
}
|
||||
return commandInfos[strings.ToUpper(commandName)]
|
||||
}
|
|
@ -1,651 +0,0 @@
|
|||
// Copyright 2012 Gary Burd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package redis
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// conn is the low-level implementation of Conn
|
||||
type conn struct {
|
||||
// Shared
|
||||
mu sync.Mutex
|
||||
pending int
|
||||
err error
|
||||
conn net.Conn
|
||||
|
||||
// Read
|
||||
readTimeout time.Duration
|
||||
br *bufio.Reader
|
||||
|
||||
// Write
|
||||
writeTimeout time.Duration
|
||||
bw *bufio.Writer
|
||||
|
||||
// Scratch space for formatting argument length.
|
||||
// '*' or '$', length, "\r\n"
|
||||
lenScratch [32]byte
|
||||
|
||||
// Scratch space for formatting integers and floats.
|
||||
numScratch [40]byte
|
||||
}
|
||||
|
||||
// DialTimeout acts like Dial but takes timeouts for establishing the
|
||||
// connection to the server, writing a command and reading a reply.
|
||||
//
|
||||
// Deprecated: Use Dial with options instead.
|
||||
func DialTimeout(network, address string, connectTimeout, readTimeout, writeTimeout time.Duration) (Conn, error) {
|
||||
return Dial(network, address,
|
||||
DialConnectTimeout(connectTimeout),
|
||||
DialReadTimeout(readTimeout),
|
||||
DialWriteTimeout(writeTimeout))
|
||||
}
|
||||
|
||||
// DialOption specifies an option for dialing a Redis server.
|
||||
type DialOption struct {
|
||||
f func(*dialOptions)
|
||||
}
|
||||
|
||||
type dialOptions struct {
|
||||
readTimeout time.Duration
|
||||
writeTimeout time.Duration
|
||||
dialer *net.Dialer
|
||||
dial func(network, addr string) (net.Conn, error)
|
||||
db int
|
||||
password string
|
||||
useTLS bool
|
||||
skipVerify bool
|
||||
tlsConfig *tls.Config
|
||||
}
|
||||
|
||||
// DialReadTimeout specifies the timeout for reading a single command reply.
|
||||
func DialReadTimeout(d time.Duration) DialOption {
|
||||
return DialOption{func(do *dialOptions) {
|
||||
do.readTimeout = d
|
||||
}}
|
||||
}
|
||||
|
||||
// DialWriteTimeout specifies the timeout for writing a single command.
|
||||
func DialWriteTimeout(d time.Duration) DialOption {
|
||||
return DialOption{func(do *dialOptions) {
|
||||
do.writeTimeout = d
|
||||
}}
|
||||
}
|
||||
|
||||
// DialConnectTimeout specifies the timeout for connecting to the Redis server when
|
||||
// no DialNetDial option is specified.
|
||||
func DialConnectTimeout(d time.Duration) DialOption {
|
||||
return DialOption{func(do *dialOptions) {
|
||||
do.dialer.Timeout = d
|
||||
}}
|
||||
}
|
||||
|
||||
// DialKeepAlive specifies the keep-alive period for TCP connections to the Redis server
|
||||
// when no DialNetDial option is specified.
|
||||
// If zero, keep-alives are not enabled. If no DialKeepAlive option is specified then
|
||||
// the default of 5 minutes is used to ensure that half-closed TCP sessions are detected.
|
||||
func DialKeepAlive(d time.Duration) DialOption {
|
||||
return DialOption{func(do *dialOptions) {
|
||||
do.dialer.KeepAlive = d
|
||||
}}
|
||||
}
|
||||
|
||||
// DialNetDial specifies a custom dial function for creating TCP
|
||||
// connections, otherwise a net.Dialer customized via the other options is used.
|
||||
// DialNetDial overrides DialConnectTimeout and DialKeepAlive.
|
||||
func DialNetDial(dial func(network, addr string) (net.Conn, error)) DialOption {
|
||||
return DialOption{func(do *dialOptions) {
|
||||
do.dial = dial
|
||||
}}
|
||||
}
|
||||
|
||||
// DialDatabase specifies the database to select when dialing a connection.
|
||||
func DialDatabase(db int) DialOption {
|
||||
return DialOption{func(do *dialOptions) {
|
||||
do.db = db
|
||||
}}
|
||||
}
|
||||
|
||||
// DialPassword specifies the password to use when connecting to
|
||||
// the Redis server.
|
||||
func DialPassword(password string) DialOption {
|
||||
return DialOption{func(do *dialOptions) {
|
||||
do.password = password
|
||||
}}
|
||||
}
|
||||
|
||||
// DialTLSConfig specifies the config to use when a TLS connection is dialed.
|
||||
// Has no effect when not dialing a TLS connection.
|
||||
func DialTLSConfig(c *tls.Config) DialOption {
|
||||
return DialOption{func(do *dialOptions) {
|
||||
do.tlsConfig = c
|
||||
}}
|
||||
}
|
||||
|
||||
// DialTLSSkipVerify disables server name verification when connecting over
|
||||
// TLS. Has no effect when not dialing a TLS connection.
|
||||
func DialTLSSkipVerify(skip bool) DialOption {
|
||||
return DialOption{func(do *dialOptions) {
|
||||
do.skipVerify = skip
|
||||
}}
|
||||
}
|
||||
|
||||
// DialUseTLS specifies whether TLS should be used when connecting to the
|
||||
// server. This option is ignore by DialURL.
|
||||
func DialUseTLS(useTLS bool) DialOption {
|
||||
return DialOption{func(do *dialOptions) {
|
||||
do.useTLS = useTLS
|
||||
}}
|
||||
}
|
||||
|
||||
// Dial connects to the Redis server at the given network and
|
||||
// address using the specified options.
|
||||
func Dial(network, address string, options ...DialOption) (Conn, error) {
|
||||
do := dialOptions{
|
||||
dialer: &net.Dialer{
|
||||
KeepAlive: time.Minute * 5,
|
||||
},
|
||||
}
|
||||
for _, option := range options {
|
||||
option.f(&do)
|
||||
}
|
||||
if do.dial == nil {
|
||||
do.dial = do.dialer.Dial
|
||||
}
|
||||
|
||||
netConn, err := do.dial(network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if do.useTLS {
|
||||
tlsConfig := cloneTLSClientConfig(do.tlsConfig, do.skipVerify)
|
||||
if tlsConfig.ServerName == "" {
|
||||
host, _, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
netConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig.ServerName = host
|
||||
}
|
||||
|
||||
tlsConn := tls.Client(netConn, tlsConfig)
|
||||
if err := tlsConn.Handshake(); err != nil {
|
||||
netConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
netConn = tlsConn
|
||||
}
|
||||
|
||||
c := &conn{
|
||||
conn: netConn,
|
||||
bw: bufio.NewWriter(netConn),
|
||||
br: bufio.NewReader(netConn),
|
||||
readTimeout: do.readTimeout,
|
||||
writeTimeout: do.writeTimeout,
|
||||
}
|
||||
|
||||
if do.password != "" {
|
||||
if _, err := c.Do("AUTH", do.password); err != nil {
|
||||
netConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if do.db != 0 {
|
||||
if _, err := c.Do("SELECT", do.db); err != nil {
|
||||
netConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
var pathDBRegexp = regexp.MustCompile(`/(\d*)\z`)
|
||||
|
||||
// DialURL connects to a Redis server at the given URL using the Redis
|
||||
// URI scheme. URLs should follow the draft IANA specification for the
|
||||
// scheme (https://www.iana.org/assignments/uri-schemes/prov/redis).
|
||||
func DialURL(rawurl string, options ...DialOption) (Conn, error) {
|
||||
u, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u.Scheme != "redis" && u.Scheme != "rediss" {
|
||||
return nil, fmt.Errorf("invalid redis URL scheme: %s", u.Scheme)
|
||||
}
|
||||
|
||||
// As per the IANA draft spec, the host defaults to localhost and
|
||||
// the port defaults to 6379.
|
||||
host, port, err := net.SplitHostPort(u.Host)
|
||||
if err != nil {
|
||||
// assume port is missing
|
||||
host = u.Host
|
||||
port = "6379"
|
||||
}
|
||||
if host == "" {
|
||||
host = "localhost"
|
||||
}
|
||||
address := net.JoinHostPort(host, port)
|
||||
|
||||
if u.User != nil {
|
||||
password, isSet := u.User.Password()
|
||||
if isSet {
|
||||
options = append(options, DialPassword(password))
|
||||
}
|
||||
}
|
||||
|
||||
match := pathDBRegexp.FindStringSubmatch(u.Path)
|
||||
if len(match) == 2 {
|
||||
db := 0
|
||||
if len(match[1]) > 0 {
|
||||
db, err = strconv.Atoi(match[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid database: %s", u.Path[1:])
|
||||
}
|
||||
}
|
||||
if db != 0 {
|
||||
options = append(options, DialDatabase(db))
|
||||
}
|
||||
} else if u.Path != "" {
|
||||
return nil, fmt.Errorf("invalid database: %s", u.Path[1:])
|
||||
}
|
||||
|
||||
options = append(options, DialUseTLS(u.Scheme == "rediss"))
|
||||
|
||||
return Dial("tcp", address, options...)
|
||||
}
|
||||
|
||||
// NewConn returns a new Redigo connection for the given net connection.
|
||||
func NewConn(netConn net.Conn, readTimeout, writeTimeout time.Duration) Conn {
|
||||
return &conn{
|
||||
conn: netConn,
|
||||
bw: bufio.NewWriter(netConn),
|
||||
br: bufio.NewReader(netConn),
|
||||
readTimeout: readTimeout,
|
||||
writeTimeout: writeTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *conn) Close() error {
|
||||
c.mu.Lock()
|
||||
err := c.err
|
||||
if c.err == nil {
|
||||
c.err = errors.New("redigo: closed")
|
||||
err = c.conn.Close()
|
||||
}
|
||||
c.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *conn) fatal(err error) error {
|
||||
c.mu.Lock()
|
||||
if c.err == nil {
|
||||
c.err = err
|
||||
// Close connection to force errors on subsequent calls and to unblock
|
||||
// other reader or writer.
|
||||
c.conn.Close()
|
||||
}
|
||||
c.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *conn) Err() error {
|
||||
c.mu.Lock()
|
||||
err := c.err
|
||||
c.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *conn) writeLen(prefix byte, n int) error {
|
||||
c.lenScratch[len(c.lenScratch)-1] = '\n'
|
||||
c.lenScratch[len(c.lenScratch)-2] = '\r'
|
||||
i := len(c.lenScratch) - 3
|
||||
for {
|
||||
c.lenScratch[i] = byte('0' + n%10)
|
||||
i -= 1
|
||||
n = n / 10
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
c.lenScratch[i] = prefix
|
||||
_, err := c.bw.Write(c.lenScratch[i:])
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *conn) writeString(s string) error {
|
||||
c.writeLen('$', len(s))
|
||||
c.bw.WriteString(s)
|
||||
_, err := c.bw.WriteString("\r\n")
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *conn) writeBytes(p []byte) error {
|
||||
c.writeLen('$', len(p))
|
||||
c.bw.Write(p)
|
||||
_, err := c.bw.WriteString("\r\n")
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *conn) writeInt64(n int64) error {
|
||||
return c.writeBytes(strconv.AppendInt(c.numScratch[:0], n, 10))
|
||||
}
|
||||
|
||||
func (c *conn) writeFloat64(n float64) error {
|
||||
return c.writeBytes(strconv.AppendFloat(c.numScratch[:0], n, 'g', -1, 64))
|
||||
}
|
||||
|
||||
func (c *conn) writeCommand(cmd string, args []interface{}) error {
|
||||
c.writeLen('*', 1+len(args))
|
||||
if err := c.writeString(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, arg := range args {
|
||||
if err := c.writeArg(arg, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *conn) writeArg(arg interface{}, argumentTypeOK bool) (err error) {
|
||||
switch arg := arg.(type) {
|
||||
case string:
|
||||
return c.writeString(arg)
|
||||
case []byte:
|
||||
return c.writeBytes(arg)
|
||||
case int:
|
||||
return c.writeInt64(int64(arg))
|
||||
case int64:
|
||||
return c.writeInt64(arg)
|
||||
case float64:
|
||||
return c.writeFloat64(arg)
|
||||
case bool:
|
||||
if arg {
|
||||
return c.writeString("1")
|
||||
} else {
|
||||
return c.writeString("0")
|
||||
}
|
||||
case nil:
|
||||
return c.writeString("")
|
||||
case Argument:
|
||||
if argumentTypeOK {
|
||||
return c.writeArg(arg.RedisArg(), false)
|
||||
}
|
||||
// See comment in default clause below.
|
||||
var buf bytes.Buffer
|
||||
fmt.Fprint(&buf, arg)
|
||||
return c.writeBytes(buf.Bytes())
|
||||
default:
|
||||
// This default clause is intended to handle builtin numeric types.
|
||||
// The function should return an error for other types, but this is not
|
||||
// done for compatibility with previous versions of the package.
|
||||
var buf bytes.Buffer
|
||||
fmt.Fprint(&buf, arg)
|
||||
return c.writeBytes(buf.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
type protocolError string
|
||||
|
||||
func (pe protocolError) Error() string {
|
||||
return fmt.Sprintf("redigo: %s (possible server error or unsupported concurrent read by application)", string(pe))
|
||||
}
|
||||
|
||||
func (c *conn) readLine() ([]byte, error) {
|
||||
p, err := c.br.ReadSlice('\n')
|
||||
if err == bufio.ErrBufferFull {
|
||||
return nil, protocolError("long response line")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
i := len(p) - 2
|
||||
if i < 0 || p[i] != '\r' {
|
||||
return nil, protocolError("bad response line terminator")
|
||||
}
|
||||
return p[:i], nil
|
||||
}
|
||||
|
||||
// parseLen parses bulk string and array lengths.
|
||||
func parseLen(p []byte) (int, error) {
|
||||
if len(p) == 0 {
|
||||
return -1, protocolError("malformed length")
|
||||
}
|
||||
|
||||
if p[0] == '-' && len(p) == 2 && p[1] == '1' {
|
||||
// handle $-1 and $-1 null replies.
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
var n int
|
||||
for _, b := range p {
|
||||
n *= 10
|
||||
if b < '0' || b > '9' {
|
||||
return -1, protocolError("illegal bytes in length")
|
||||
}
|
||||
n += int(b - '0')
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// parseInt parses an integer reply.
|
||||
func parseInt(p []byte) (interface{}, error) {
|
||||
if len(p) == 0 {
|
||||
return 0, protocolError("malformed integer")
|
||||
}
|
||||
|
||||
var negate bool
|
||||
if p[0] == '-' {
|
||||
negate = true
|
||||
p = p[1:]
|
||||
if len(p) == 0 {
|
||||
return 0, protocolError("malformed integer")
|
||||
}
|
||||
}
|
||||
|
||||
var n int64
|
||||
for _, b := range p {
|
||||
n *= 10
|
||||
if b < '0' || b > '9' {
|
||||
return 0, protocolError("illegal bytes in length")
|
||||
}
|
||||
n += int64(b - '0')
|
||||
}
|
||||
|
||||
if negate {
|
||||
n = -n
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
var (
|
||||
okReply interface{} = "OK"
|
||||
pongReply interface{} = "PONG"
|
||||
)
|
||||
|
||||
func (c *conn) readReply() (interface{}, error) {
|
||||
line, err := c.readLine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) == 0 {
|
||||
return nil, protocolError("short response line")
|
||||
}
|
||||
switch line[0] {
|
||||
case '+':
|
||||
switch {
|
||||
case len(line) == 3 && line[1] == 'O' && line[2] == 'K':
|
||||
// Avoid allocation for frequent "+OK" response.
|
||||
return okReply, nil
|
||||
case len(line) == 5 && line[1] == 'P' && line[2] == 'O' && line[3] == 'N' && line[4] == 'G':
|
||||
// Avoid allocation in PING command benchmarks :)
|
||||
return pongReply, nil
|
||||
default:
|
||||
return string(line[1:]), nil
|
||||
}
|
||||
case '-':
|
||||
return Error(string(line[1:])), nil
|
||||
case ':':
|
||||
return parseInt(line[1:])
|
||||
case '$':
|
||||
n, err := parseLen(line[1:])
|
||||
if n < 0 || err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p := make([]byte, n)
|
||||
_, err = io.ReadFull(c.br, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if line, err := c.readLine(); err != nil {
|
||||
return nil, err
|
||||
} else if len(line) != 0 {
|
||||
return nil, protocolError("bad bulk string format")
|
||||
}
|
||||
return p, nil
|
||||
case '*':
|
||||
n, err := parseLen(line[1:])
|
||||
if n < 0 || err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r := make([]interface{}, n)
|
||||
for i := range r {
|
||||
r[i], err = c.readReply()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
return nil, protocolError("unexpected response line")
|
||||
}
|
||||
|
||||
func (c *conn) Send(cmd string, args ...interface{}) error {
|
||||
c.mu.Lock()
|
||||
c.pending += 1
|
||||
c.mu.Unlock()
|
||||
if c.writeTimeout != 0 {
|
||||
c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
|
||||
}
|
||||
if err := c.writeCommand(cmd, args); err != nil {
|
||||
return c.fatal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *conn) Flush() error {
|
||||
if c.writeTimeout != 0 {
|
||||
c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
|
||||
}
|
||||
if err := c.bw.Flush(); err != nil {
|
||||
return c.fatal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *conn) Receive() (reply interface{}, err error) {
|
||||
if c.readTimeout != 0 {
|
||||
c.conn.SetReadDeadline(time.Now().Add(c.readTimeout))
|
||||
}
|
||||
if reply, err = c.readReply(); err != nil {
|
||||
return nil, c.fatal(err)
|
||||
}
|
||||
// When using pub/sub, the number of receives can be greater than the
|
||||
// number of sends. To enable normal use of the connection after
|
||||
// unsubscribing from all channels, we do not decrement pending to a
|
||||
// negative value.
|
||||
//
|
||||
// The pending field is decremented after the reply is read to handle the
|
||||
// case where Receive is called before Send.
|
||||
c.mu.Lock()
|
||||
if c.pending > 0 {
|
||||
c.pending -= 1
|
||||
}
|
||||
c.mu.Unlock()
|
||||
if err, ok := reply.(Error); ok {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *conn) Do(cmd string, args ...interface{}) (interface{}, error) {
|
||||
c.mu.Lock()
|
||||
pending := c.pending
|
||||
c.pending = 0
|
||||
c.mu.Unlock()
|
||||
|
||||
if cmd == "" && pending == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if c.writeTimeout != 0 {
|
||||
c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
|
||||
}
|
||||
|
||||
if cmd != "" {
|
||||
if err := c.writeCommand(cmd, args); err != nil {
|
||||
return nil, c.fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.bw.Flush(); err != nil {
|
||||
return nil, c.fatal(err)
|
||||
}
|
||||
|
||||
if c.readTimeout != 0 {
|
||||
c.conn.SetReadDeadline(time.Now().Add(c.readTimeout))
|
||||
}
|
||||
|
||||
if cmd == "" {
|
||||
reply := make([]interface{}, pending)
|
||||
for i := range reply {
|
||||
r, e := c.readReply()
|
||||
if e != nil {
|
||||
return nil, c.fatal(e)
|
||||
}
|
||||
reply[i] = r
|
||||
}
|
||||
return reply, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
var reply interface{}
|
||||
for i := 0; i <= pending; i++ {
|
||||
var e error
|
||||
if reply, e = c.readReply(); e != nil {
|
||||
return nil, c.fatal(e)
|
||||
}
|
||||
if e, ok := reply.(Error); ok && err == nil {
|
||||
err = e
|
||||
}
|
||||
}
|
||||
return reply, err
|
||||
}
|
|
@ -1,177 +0,0 @@
|
|||
// Copyright 2012 Gary Burd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
// Package redis is a client for the Redis database.
|
||||
//
|
||||
// The Redigo FAQ (https://github.com/garyburd/redigo/wiki/FAQ) contains more
|
||||
// documentation about this package.
|
||||
//
|
||||
// Connections
|
||||
//
|
||||
// The Conn interface is the primary interface for working with Redis.
|
||||
// Applications create connections by calling the Dial, DialWithTimeout or
|
||||
// NewConn functions. In the future, functions will be added for creating
|
||||
// sharded and other types of connections.
|
||||
//
|
||||
// The application must call the connection Close method when the application
|
||||
// is done with the connection.
|
||||
//
|
||||
// Executing Commands
|
||||
//
|
||||
// The Conn interface has a generic method for executing Redis commands:
|
||||
//
|
||||
// Do(commandName string, args ...interface{}) (reply interface{}, err error)
|
||||
//
|
||||
// The Redis command reference (http://redis.io/commands) lists the available
|
||||
// commands. An example of using the Redis APPEND command is:
|
||||
//
|
||||
// n, err := conn.Do("APPEND", "key", "value")
|
||||
//
|
||||
// The Do method converts command arguments to bulk strings for transmission
|
||||
// to the server as follows:
|
||||
//
|
||||
// Go Type Conversion
|
||||
// []byte Sent as is
|
||||
// string Sent as is
|
||||
// int, int64 strconv.FormatInt(v)
|
||||
// float64 strconv.FormatFloat(v, 'g', -1, 64)
|
||||
// bool true -> "1", false -> "0"
|
||||
// nil ""
|
||||
// all other types fmt.Fprint(w, v)
|
||||
//
|
||||
// Redis command reply types are represented using the following Go types:
|
||||
//
|
||||
// Redis type Go type
|
||||
// error redis.Error
|
||||
// integer int64
|
||||
// simple string string
|
||||
// bulk string []byte or nil if value not present.
|
||||
// array []interface{} or nil if value not present.
|
||||
//
|
||||
// Use type assertions or the reply helper functions to convert from
|
||||
// interface{} to the specific Go type for the command result.
|
||||
//
|
||||
// Pipelining
|
||||
//
|
||||
// Connections support pipelining using the Send, Flush and Receive methods.
|
||||
//
|
||||
// Send(commandName string, args ...interface{}) error
|
||||
// Flush() error
|
||||
// Receive() (reply interface{}, err error)
|
||||
//
|
||||
// Send writes the command to the connection's output buffer. Flush flushes the
|
||||
// connection's output buffer to the server. Receive reads a single reply from
|
||||
// the server. The following example shows a simple pipeline.
|
||||
//
|
||||
// c.Send("SET", "foo", "bar")
|
||||
// c.Send("GET", "foo")
|
||||
// c.Flush()
|
||||
// c.Receive() // reply from SET
|
||||
// v, err = c.Receive() // reply from GET
|
||||
//
|
||||
// The Do method combines the functionality of the Send, Flush and Receive
|
||||
// methods. The Do method starts by writing the command and flushing the output
|
||||
// buffer. Next, the Do method receives all pending replies including the reply
|
||||
// for the command just sent by Do. If any of the received replies is an error,
|
||||
// then Do returns the error. If there are no errors, then Do returns the last
|
||||
// reply. If the command argument to the Do method is "", then the Do method
|
||||
// will flush the output buffer and receive pending replies without sending a
|
||||
// command.
|
||||
//
|
||||
// Use the Send and Do methods to implement pipelined transactions.
|
||||
//
|
||||
// c.Send("MULTI")
|
||||
// c.Send("INCR", "foo")
|
||||
// c.Send("INCR", "bar")
|
||||
// r, err := c.Do("EXEC")
|
||||
// fmt.Println(r) // prints [1, 1]
|
||||
//
|
||||
// Concurrency
|
||||
//
|
||||
// Connections support one concurrent caller to the Receive method and one
|
||||
// concurrent caller to the Send and Flush methods. No other concurrency is
|
||||
// supported including concurrent calls to the Do method.
|
||||
//
|
||||
// For full concurrent access to Redis, use the thread-safe Pool to get, use
|
||||
// and release a connection from within a goroutine. Connections returned from
|
||||
// a Pool have the concurrency restrictions described in the previous
|
||||
// paragraph.
|
||||
//
|
||||
// Publish and Subscribe
|
||||
//
|
||||
// Use the Send, Flush and Receive methods to implement Pub/Sub subscribers.
|
||||
//
|
||||
// c.Send("SUBSCRIBE", "example")
|
||||
// c.Flush()
|
||||
// for {
|
||||
// reply, err := c.Receive()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// // process pushed message
|
||||
// }
|
||||
//
|
||||
// The PubSubConn type wraps a Conn with convenience methods for implementing
|
||||
// subscribers. The Subscribe, PSubscribe, Unsubscribe and PUnsubscribe methods
|
||||
// send and flush a subscription management command. The receive method
|
||||
// converts a pushed message to convenient types for use in a type switch.
|
||||
//
|
||||
// psc := redis.PubSubConn{Conn: c}
|
||||
// psc.Subscribe("example")
|
||||
// for {
|
||||
// switch v := psc.Receive().(type) {
|
||||
// case redis.Message:
|
||||
// fmt.Printf("%s: message: %s\n", v.Channel, v.Data)
|
||||
// case redis.Subscription:
|
||||
// fmt.Printf("%s: %s %d\n", v.Channel, v.Kind, v.Count)
|
||||
// case error:
|
||||
// return v
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Reply Helpers
|
||||
//
|
||||
// The Bool, Int, Bytes, String, Strings and Values functions convert a reply
|
||||
// to a value of a specific type. To allow convenient wrapping of calls to the
|
||||
// connection Do and Receive methods, the functions take a second argument of
|
||||
// type error. If the error is non-nil, then the helper function returns the
|
||||
// error. If the error is nil, the function converts the reply to the specified
|
||||
// type:
|
||||
//
|
||||
// exists, err := redis.Bool(c.Do("EXISTS", "foo"))
|
||||
// if err != nil {
|
||||
// // handle error return from c.Do or type conversion error.
|
||||
// }
|
||||
//
|
||||
// The Scan function converts elements of a array reply to Go types:
|
||||
//
|
||||
// var value1 int
|
||||
// var value2 string
|
||||
// reply, err := redis.Values(c.Do("MGET", "key1", "key2"))
|
||||
// if err != nil {
|
||||
// // handle error
|
||||
// }
|
||||
// if _, err := redis.Scan(reply, &value1, &value2); err != nil {
|
||||
// // handle error
|
||||
// }
|
||||
//
|
||||
// Errors
|
||||
//
|
||||
// Connection methods return error replies from the server as type redis.Error.
|
||||
//
|
||||
// Call the connection Err() method to determine if the connection encountered
|
||||
// non-recoverable error such as a network error or protocol parsing error. If
|
||||
// Err() returns a non-nil value, then the connection is not usable and should
|
||||
// be closed.
|
||||
package redis // import "github.com/garyburd/redigo/redis"
|
|
@ -1,33 +0,0 @@
|
|||
// +build go1.7
|
||||
|
||||
package redis
|
||||
|
||||
import "crypto/tls"
|
||||
|
||||
// similar cloneTLSClientConfig in the stdlib, but also honor skipVerify for the nil case
|
||||
func cloneTLSClientConfig(cfg *tls.Config, skipVerify bool) *tls.Config {
|
||||
if cfg == nil {
|
||||
return &tls.Config{InsecureSkipVerify: skipVerify}
|
||||
}
|
||||
return &tls.Config{
|
||||
Rand: cfg.Rand,
|
||||
Time: cfg.Time,
|
||||
Certificates: cfg.Certificates,
|
||||
NameToCertificate: cfg.NameToCertificate,
|
||||
GetCertificate: cfg.GetCertificate,
|
||||
RootCAs: cfg.RootCAs,
|
||||
NextProtos: cfg.NextProtos,
|
||||
ServerName: cfg.ServerName,
|
||||
ClientAuth: cfg.ClientAuth,
|
||||
ClientCAs: cfg.ClientCAs,
|
||||
InsecureSkipVerify: cfg.InsecureSkipVerify,
|
||||
CipherSuites: cfg.CipherSuites,
|
||||
PreferServerCipherSuites: cfg.PreferServerCipherSuites,
|
||||
ClientSessionCache: cfg.ClientSessionCache,
|
||||
MinVersion: cfg.MinVersion,
|
||||
MaxVersion: cfg.MaxVersion,
|
||||
CurvePreferences: cfg.CurvePreferences,
|
||||
DynamicRecordSizingDisabled: cfg.DynamicRecordSizingDisabled,
|
||||
Renegotiation: cfg.Renegotiation,
|
||||
}
|
||||
}
|
|
@ -1,117 +0,0 @@
|
|||
// Copyright 2012 Gary Burd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package redis
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
// NewLoggingConn returns a logging wrapper around a connection.
|
||||
func NewLoggingConn(conn Conn, logger *log.Logger, prefix string) Conn {
|
||||
if prefix != "" {
|
||||
prefix = prefix + "."
|
||||
}
|
||||
return &loggingConn{conn, logger, prefix}
|
||||
}
|
||||
|
||||
type loggingConn struct {
|
||||
Conn
|
||||
logger *log.Logger
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (c *loggingConn) Close() error {
|
||||
err := c.Conn.Close()
|
||||
var buf bytes.Buffer
|
||||
fmt.Fprintf(&buf, "%sClose() -> (%v)", c.prefix, err)
|
||||
c.logger.Output(2, buf.String())
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *loggingConn) printValue(buf *bytes.Buffer, v interface{}) {
|
||||
const chop = 32
|
||||
switch v := v.(type) {
|
||||
case []byte:
|
||||
if len(v) > chop {
|
||||
fmt.Fprintf(buf, "%q...", v[:chop])
|
||||
} else {
|
||||
fmt.Fprintf(buf, "%q", v)
|
||||
}
|
||||
case string:
|
||||
if len(v) > chop {
|
||||
fmt.Fprintf(buf, "%q...", v[:chop])
|
||||
} else {
|
||||
fmt.Fprintf(buf, "%q", v)
|
||||
}
|
||||
case []interface{}:
|
||||
if len(v) == 0 {
|
||||
buf.WriteString("[]")
|
||||
} else {
|
||||
sep := "["
|
||||
fin := "]"
|
||||
if len(v) > chop {
|
||||
v = v[:chop]
|
||||
fin = "...]"
|
||||
}
|
||||
for _, vv := range v {
|
||||
buf.WriteString(sep)
|
||||
c.printValue(buf, vv)
|
||||
sep = ", "
|
||||
}
|
||||
buf.WriteString(fin)
|
||||
}
|
||||
default:
|
||||
fmt.Fprint(buf, v)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *loggingConn) print(method, commandName string, args []interface{}, reply interface{}, err error) {
|
||||
var buf bytes.Buffer
|
||||
fmt.Fprintf(&buf, "%s%s(", c.prefix, method)
|
||||
if method != "Receive" {
|
||||
buf.WriteString(commandName)
|
||||
for _, arg := range args {
|
||||
buf.WriteString(", ")
|
||||
c.printValue(&buf, arg)
|
||||
}
|
||||
}
|
||||
buf.WriteString(") -> (")
|
||||
if method != "Send" {
|
||||
c.printValue(&buf, reply)
|
||||
buf.WriteString(", ")
|
||||
}
|
||||
fmt.Fprintf(&buf, "%v)", err)
|
||||
c.logger.Output(3, buf.String())
|
||||
}
|
||||
|
||||
func (c *loggingConn) Do(commandName string, args ...interface{}) (interface{}, error) {
|
||||
reply, err := c.Conn.Do(commandName, args...)
|
||||
c.print("Do", commandName, args, reply, err)
|
||||
return reply, err
|
||||
}
|
||||
|
||||
func (c *loggingConn) Send(commandName string, args ...interface{}) error {
|
||||
err := c.Conn.Send(commandName, args...)
|
||||
c.print("Send", commandName, args, nil, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *loggingConn) Receive() (interface{}, error) {
|
||||
reply, err := c.Conn.Receive()
|
||||
c.print("Receive", "", nil, reply, err)
|
||||
return reply, err
|
||||
}
|
|
@ -1,442 +0,0 @@
|
|||
// Copyright 2012 Gary Burd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package redis
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"container/list"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"errors"
|
||||
"io"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/garyburd/redigo/internal"
|
||||
)
|
||||
|
||||
var nowFunc = time.Now // for testing
|
||||
|
||||
// ErrPoolExhausted is returned from a pool connection method (Do, Send,
|
||||
// Receive, Flush, Err) when the maximum number of database connections in the
|
||||
// pool has been reached.
|
||||
var ErrPoolExhausted = errors.New("redigo: connection pool exhausted")
|
||||
|
||||
var (
|
||||
errPoolClosed = errors.New("redigo: connection pool closed")
|
||||
errConnClosed = errors.New("redigo: connection closed")
|
||||
)
|
||||
|
||||
// Pool maintains a pool of connections. The application calls the Get method
|
||||
// to get a connection from the pool and the connection's Close method to
|
||||
// return the connection's resources to the pool.
|
||||
//
|
||||
// The following example shows how to use a pool in a web application. The
|
||||
// application creates a pool at application startup and makes it available to
|
||||
// request handlers using a package level variable. The pool configuration used
|
||||
// here is an example, not a recommendation.
|
||||
//
|
||||
// func newPool(addr string) *redis.Pool {
|
||||
// return &redis.Pool{
|
||||
// MaxIdle: 3,
|
||||
// IdleTimeout: 240 * time.Second,
|
||||
// Dial: func () (redis.Conn, error) { return redis.Dial("tcp", addr) },
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// var (
|
||||
// pool *redis.Pool
|
||||
// redisServer = flag.String("redisServer", ":6379", "")
|
||||
// )
|
||||
//
|
||||
// func main() {
|
||||
// flag.Parse()
|
||||
// pool = newPool(*redisServer)
|
||||
// ...
|
||||
// }
|
||||
//
|
||||
// A request handler gets a connection from the pool and closes the connection
|
||||
// when the handler is done:
|
||||
//
|
||||
// func serveHome(w http.ResponseWriter, r *http.Request) {
|
||||
// conn := pool.Get()
|
||||
// defer conn.Close()
|
||||
// ...
|
||||
// }
|
||||
//
|
||||
// Use the Dial function to authenticate connections with the AUTH command or
|
||||
// select a database with the SELECT command:
|
||||
//
|
||||
// pool := &redis.Pool{
|
||||
// // Other pool configuration not shown in this example.
|
||||
// Dial: func () (redis.Conn, error) {
|
||||
// c, err := redis.Dial("tcp", server)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// if _, err := c.Do("AUTH", password); err != nil {
|
||||
// c.Close()
|
||||
// return nil, err
|
||||
// }
|
||||
// if _, err := c.Do("SELECT", db); err != nil {
|
||||
// c.Close()
|
||||
// return nil, err
|
||||
// }
|
||||
// return c, nil
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Use the TestOnBorrow function to check the health of an idle connection
|
||||
// before the connection is returned to the application. This example PINGs
|
||||
// connections that have been idle more than a minute:
|
||||
//
|
||||
// pool := &redis.Pool{
|
||||
// // Other pool configuration not shown in this example.
|
||||
// TestOnBorrow: func(c redis.Conn, t time.Time) error {
|
||||
// if time.Since(t) < time.Minute {
|
||||
// return nil
|
||||
// }
|
||||
// _, err := c.Do("PING")
|
||||
// return err
|
||||
// },
|
||||
// }
|
||||
//
|
||||
type Pool struct {
|
||||
// Dial is an application supplied function for creating and configuring a
|
||||
// connection.
|
||||
//
|
||||
// The connection returned from Dial must not be in a special state
|
||||
// (subscribed to pubsub channel, transaction started, ...).
|
||||
Dial func() (Conn, error)
|
||||
|
||||
// TestOnBorrow is an optional application supplied function for checking
|
||||
// the health of an idle connection before the connection is used again by
|
||||
// the application. Argument t is the time that the connection was returned
|
||||
// to the pool. If the function returns an error, then the connection is
|
||||
// closed.
|
||||
TestOnBorrow func(c Conn, t time.Time) error
|
||||
|
||||
// Maximum number of idle connections in the pool.
|
||||
MaxIdle int
|
||||
|
||||
// Maximum number of connections allocated by the pool at a given time.
|
||||
// When zero, there is no limit on the number of connections in the pool.
|
||||
MaxActive int
|
||||
|
||||
// Close connections after remaining idle for this duration. If the value
|
||||
// is zero, then idle connections are not closed. Applications should set
|
||||
// the timeout to a value less than the server's timeout.
|
||||
IdleTimeout time.Duration
|
||||
|
||||
// If Wait is true and the pool is at the MaxActive limit, then Get() waits
|
||||
// for a connection to be returned to the pool before returning.
|
||||
Wait bool
|
||||
|
||||
// mu protects fields defined below.
|
||||
mu sync.Mutex
|
||||
cond *sync.Cond
|
||||
closed bool
|
||||
active int
|
||||
|
||||
// Stack of idleConn with most recently used at the front.
|
||||
idle list.List
|
||||
}
|
||||
|
||||
type idleConn struct {
|
||||
c Conn
|
||||
t time.Time
|
||||
}
|
||||
|
||||
// NewPool creates a new pool.
|
||||
//
|
||||
// Deprecated: Initialize the Pool directory as shown in the example.
|
||||
func NewPool(newFn func() (Conn, error), maxIdle int) *Pool {
|
||||
return &Pool{Dial: newFn, MaxIdle: maxIdle}
|
||||
}
|
||||
|
||||
// Get gets a connection. The application must close the returned connection.
|
||||
// This method always returns a valid connection so that applications can defer
|
||||
// error handling to the first use of the connection. If there is an error
|
||||
// getting an underlying connection, then the connection Err, Do, Send, Flush
|
||||
// and Receive methods return that error.
|
||||
func (p *Pool) Get() Conn {
|
||||
c, err := p.get()
|
||||
if err != nil {
|
||||
return errorConnection{err}
|
||||
}
|
||||
return &pooledConnection{p: p, c: c}
|
||||
}
|
||||
|
||||
// PoolStats contains pool statistics.
|
||||
type PoolStats struct {
|
||||
// ActiveCount is the number of connections in the pool. The count includes idle connections and connections in use.
|
||||
ActiveCount int
|
||||
// IdleCount is the number of idle connections in the pool.
|
||||
IdleCount int
|
||||
}
|
||||
|
||||
// Stats returns pool's statistics.
|
||||
func (p *Pool) Stats() PoolStats {
|
||||
p.mu.Lock()
|
||||
stats := PoolStats{
|
||||
ActiveCount: p.active,
|
||||
IdleCount: p.idle.Len(),
|
||||
}
|
||||
p.mu.Unlock()
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
// ActiveCount returns the number of connections in the pool. The count includes idle connections and connections in use.
|
||||
func (p *Pool) ActiveCount() int {
|
||||
p.mu.Lock()
|
||||
active := p.active
|
||||
p.mu.Unlock()
|
||||
return active
|
||||
}
|
||||
|
||||
// IdleCount returns the number of idle connections in the pool.
|
||||
func (p *Pool) IdleCount() int {
|
||||
p.mu.Lock()
|
||||
idle := p.idle.Len()
|
||||
p.mu.Unlock()
|
||||
return idle
|
||||
}
|
||||
|
||||
// Close releases the resources used by the pool.
|
||||
func (p *Pool) Close() error {
|
||||
p.mu.Lock()
|
||||
idle := p.idle
|
||||
p.idle.Init()
|
||||
p.closed = true
|
||||
p.active -= idle.Len()
|
||||
if p.cond != nil {
|
||||
p.cond.Broadcast()
|
||||
}
|
||||
p.mu.Unlock()
|
||||
for e := idle.Front(); e != nil; e = e.Next() {
|
||||
e.Value.(idleConn).c.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// release decrements the active count and signals waiters. The caller must
|
||||
// hold p.mu during the call.
|
||||
func (p *Pool) release() {
|
||||
p.active -= 1
|
||||
if p.cond != nil {
|
||||
p.cond.Signal()
|
||||
}
|
||||
}
|
||||
|
||||
// get prunes stale connections and returns a connection from the idle list or
|
||||
// creates a new connection.
|
||||
func (p *Pool) get() (Conn, error) {
|
||||
p.mu.Lock()
|
||||
|
||||
// Prune stale connections.
|
||||
|
||||
if timeout := p.IdleTimeout; timeout > 0 {
|
||||
for i, n := 0, p.idle.Len(); i < n; i++ {
|
||||
e := p.idle.Back()
|
||||
if e == nil {
|
||||
break
|
||||
}
|
||||
ic := e.Value.(idleConn)
|
||||
if ic.t.Add(timeout).After(nowFunc()) {
|
||||
break
|
||||
}
|
||||
p.idle.Remove(e)
|
||||
p.release()
|
||||
p.mu.Unlock()
|
||||
ic.c.Close()
|
||||
p.mu.Lock()
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
// Get idle connection.
|
||||
|
||||
for i, n := 0, p.idle.Len(); i < n; i++ {
|
||||
e := p.idle.Front()
|
||||
if e == nil {
|
||||
break
|
||||
}
|
||||
ic := e.Value.(idleConn)
|
||||
p.idle.Remove(e)
|
||||
test := p.TestOnBorrow
|
||||
p.mu.Unlock()
|
||||
if test == nil || test(ic.c, ic.t) == nil {
|
||||
return ic.c, nil
|
||||
}
|
||||
ic.c.Close()
|
||||
p.mu.Lock()
|
||||
p.release()
|
||||
}
|
||||
|
||||
// Check for pool closed before dialing a new connection.
|
||||
|
||||
if p.closed {
|
||||
p.mu.Unlock()
|
||||
return nil, errors.New("redigo: get on closed pool")
|
||||
}
|
||||
|
||||
// Dial new connection if under limit.
|
||||
|
||||
if p.MaxActive == 0 || p.active < p.MaxActive {
|
||||
dial := p.Dial
|
||||
p.active += 1
|
||||
p.mu.Unlock()
|
||||
c, err := dial()
|
||||
if err != nil {
|
||||
p.mu.Lock()
|
||||
p.release()
|
||||
p.mu.Unlock()
|
||||
c = nil
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
|
||||
if !p.Wait {
|
||||
p.mu.Unlock()
|
||||
return nil, ErrPoolExhausted
|
||||
}
|
||||
|
||||
if p.cond == nil {
|
||||
p.cond = sync.NewCond(&p.mu)
|
||||
}
|
||||
p.cond.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pool) put(c Conn, forceClose bool) error {
|
||||
err := c.Err()
|
||||
p.mu.Lock()
|
||||
if !p.closed && err == nil && !forceClose {
|
||||
p.idle.PushFront(idleConn{t: nowFunc(), c: c})
|
||||
if p.idle.Len() > p.MaxIdle {
|
||||
c = p.idle.Remove(p.idle.Back()).(idleConn).c
|
||||
} else {
|
||||
c = nil
|
||||
}
|
||||
}
|
||||
|
||||
if c == nil {
|
||||
if p.cond != nil {
|
||||
p.cond.Signal()
|
||||
}
|
||||
p.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
p.release()
|
||||
p.mu.Unlock()
|
||||
return c.Close()
|
||||
}
|
||||
|
||||
type pooledConnection struct {
|
||||
p *Pool
|
||||
c Conn
|
||||
state int
|
||||
}
|
||||
|
||||
var (
|
||||
sentinel []byte
|
||||
sentinelOnce sync.Once
|
||||
)
|
||||
|
||||
func initSentinel() {
|
||||
p := make([]byte, 64)
|
||||
if _, err := rand.Read(p); err == nil {
|
||||
sentinel = p
|
||||
} else {
|
||||
h := sha1.New()
|
||||
io.WriteString(h, "Oops, rand failed. Use time instead.")
|
||||
io.WriteString(h, strconv.FormatInt(time.Now().UnixNano(), 10))
|
||||
sentinel = h.Sum(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (pc *pooledConnection) Close() error {
|
||||
c := pc.c
|
||||
if _, ok := c.(errorConnection); ok {
|
||||
return nil
|
||||
}
|
||||
pc.c = errorConnection{errConnClosed}
|
||||
|
||||
if pc.state&internal.MultiState != 0 {
|
||||
c.Send("DISCARD")
|
||||
pc.state &^= (internal.MultiState | internal.WatchState)
|
||||
} else if pc.state&internal.WatchState != 0 {
|
||||
c.Send("UNWATCH")
|
||||
pc.state &^= internal.WatchState
|
||||
}
|
||||
if pc.state&internal.SubscribeState != 0 {
|
||||
c.Send("UNSUBSCRIBE")
|
||||
c.Send("PUNSUBSCRIBE")
|
||||
// To detect the end of the message stream, ask the server to echo
|
||||
// a sentinel value and read until we see that value.
|
||||
sentinelOnce.Do(initSentinel)
|
||||
c.Send("ECHO", sentinel)
|
||||
c.Flush()
|
||||
for {
|
||||
p, err := c.Receive()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if p, ok := p.([]byte); ok && bytes.Equal(p, sentinel) {
|
||||
pc.state &^= internal.SubscribeState
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
c.Do("")
|
||||
pc.p.put(c, pc.state != 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pc *pooledConnection) Err() error {
|
||||
return pc.c.Err()
|
||||
}
|
||||
|
||||
func (pc *pooledConnection) Do(commandName string, args ...interface{}) (reply interface{}, err error) {
|
||||
ci := internal.LookupCommandInfo(commandName)
|
||||
pc.state = (pc.state | ci.Set) &^ ci.Clear
|
||||
return pc.c.Do(commandName, args...)
|
||||
}
|
||||
|
||||
func (pc *pooledConnection) Send(commandName string, args ...interface{}) error {
|
||||
ci := internal.LookupCommandInfo(commandName)
|
||||
pc.state = (pc.state | ci.Set) &^ ci.Clear
|
||||
return pc.c.Send(commandName, args...)
|
||||
}
|
||||
|
||||
func (pc *pooledConnection) Flush() error {
|
||||
return pc.c.Flush()
|
||||
}
|
||||
|
||||
func (pc *pooledConnection) Receive() (reply interface{}, err error) {
|
||||
return pc.c.Receive()
|
||||
}
|
||||
|
||||
type errorConnection struct{ err error }
|
||||
|
||||
func (ec errorConnection) Do(string, ...interface{}) (interface{}, error) { return nil, ec.err }
|
||||
func (ec errorConnection) Send(string, ...interface{}) error { return ec.err }
|
||||
func (ec errorConnection) Err() error { return ec.err }
|
||||
func (ec errorConnection) Close() error { return ec.err }
|
||||
func (ec errorConnection) Flush() error { return ec.err }
|
||||
func (ec errorConnection) Receive() (interface{}, error) { return nil, ec.err }
|
|
@ -1,31 +0,0 @@
|
|||
// +build !go1.7
|
||||
|
||||
package redis
|
||||
|
||||
import "crypto/tls"
|
||||
|
||||
// similar cloneTLSClientConfig in the stdlib, but also honor skipVerify for the nil case
|
||||
func cloneTLSClientConfig(cfg *tls.Config, skipVerify bool) *tls.Config {
|
||||
if cfg == nil {
|
||||
return &tls.Config{InsecureSkipVerify: skipVerify}
|
||||
}
|
||||
return &tls.Config{
|
||||
Rand: cfg.Rand,
|
||||
Time: cfg.Time,
|
||||
Certificates: cfg.Certificates,
|
||||
NameToCertificate: cfg.NameToCertificate,
|
||||
GetCertificate: cfg.GetCertificate,
|
||||
RootCAs: cfg.RootCAs,
|
||||
NextProtos: cfg.NextProtos,
|
||||
ServerName: cfg.ServerName,
|
||||
ClientAuth: cfg.ClientAuth,
|
||||
ClientCAs: cfg.ClientCAs,
|
||||
InsecureSkipVerify: cfg.InsecureSkipVerify,
|
||||
CipherSuites: cfg.CipherSuites,
|
||||
PreferServerCipherSuites: cfg.PreferServerCipherSuites,
|
||||
ClientSessionCache: cfg.ClientSessionCache,
|
||||
MinVersion: cfg.MinVersion,
|
||||
MaxVersion: cfg.MaxVersion,
|
||||
CurvePreferences: cfg.CurvePreferences,
|
||||
}
|
||||
}
|
|
@ -1,144 +0,0 @@
|
|||
// Copyright 2012 Gary Burd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package redis
|
||||
|
||||
import "errors"
|
||||
|
||||
// Subscription represents a subscribe or unsubscribe notification.
|
||||
type Subscription struct {
|
||||
// Kind is "subscribe", "unsubscribe", "psubscribe" or "punsubscribe"
|
||||
Kind string
|
||||
|
||||
// The channel that was changed.
|
||||
Channel string
|
||||
|
||||
// The current number of subscriptions for connection.
|
||||
Count int
|
||||
}
|
||||
|
||||
// Message represents a message notification.
|
||||
type Message struct {
|
||||
// The originating channel.
|
||||
Channel string
|
||||
|
||||
// The message data.
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// PMessage represents a pmessage notification.
|
||||
type PMessage struct {
|
||||
// The matched pattern.
|
||||
Pattern string
|
||||
|
||||
// The originating channel.
|
||||
Channel string
|
||||
|
||||
// The message data.
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// Pong represents a pubsub pong notification.
|
||||
type Pong struct {
|
||||
Data string
|
||||
}
|
||||
|
||||
// PubSubConn wraps a Conn with convenience methods for subscribers.
|
||||
type PubSubConn struct {
|
||||
Conn Conn
|
||||
}
|
||||
|
||||
// Close closes the connection.
|
||||
func (c PubSubConn) Close() error {
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
// Subscribe subscribes the connection to the specified channels.
|
||||
func (c PubSubConn) Subscribe(channel ...interface{}) error {
|
||||
c.Conn.Send("SUBSCRIBE", channel...)
|
||||
return c.Conn.Flush()
|
||||
}
|
||||
|
||||
// PSubscribe subscribes the connection to the given patterns.
|
||||
func (c PubSubConn) PSubscribe(channel ...interface{}) error {
|
||||
c.Conn.Send("PSUBSCRIBE", channel...)
|
||||
return c.Conn.Flush()
|
||||
}
|
||||
|
||||
// Unsubscribe unsubscribes the connection from the given channels, or from all
|
||||
// of them if none is given.
|
||||
func (c PubSubConn) Unsubscribe(channel ...interface{}) error {
|
||||
c.Conn.Send("UNSUBSCRIBE", channel...)
|
||||
return c.Conn.Flush()
|
||||
}
|
||||
|
||||
// PUnsubscribe unsubscribes the connection from the given patterns, or from all
|
||||
// of them if none is given.
|
||||
func (c PubSubConn) PUnsubscribe(channel ...interface{}) error {
|
||||
c.Conn.Send("PUNSUBSCRIBE", channel...)
|
||||
return c.Conn.Flush()
|
||||
}
|
||||
|
||||
// Ping sends a PING to the server with the specified data.
|
||||
//
|
||||
// The connection must be subscribed to at least one channel or pattern when
|
||||
// calling this method.
|
||||
func (c PubSubConn) Ping(data string) error {
|
||||
c.Conn.Send("PING", data)
|
||||
return c.Conn.Flush()
|
||||
}
|
||||
|
||||
// Receive returns a pushed message as a Subscription, Message, PMessage, Pong
|
||||
// or error. The return value is intended to be used directly in a type switch
|
||||
// as illustrated in the PubSubConn example.
|
||||
func (c PubSubConn) Receive() interface{} {
|
||||
reply, err := Values(c.Conn.Receive())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var kind string
|
||||
reply, err = Scan(reply, &kind)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case "message":
|
||||
var m Message
|
||||
if _, err := Scan(reply, &m.Channel, &m.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
return m
|
||||
case "pmessage":
|
||||
var pm PMessage
|
||||
if _, err := Scan(reply, &pm.Pattern, &pm.Channel, &pm.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
return pm
|
||||
case "subscribe", "psubscribe", "unsubscribe", "punsubscribe":
|
||||
s := Subscription{Kind: kind}
|
||||
if _, err := Scan(reply, &s.Channel, &s.Count); err != nil {
|
||||
return err
|
||||
}
|
||||
return s
|
||||
case "pong":
|
||||
var p Pong
|
||||
if _, err := Scan(reply, &p.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
return p
|
||||
}
|
||||
return errors.New("redigo: unknown pubsub notification")
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
testdata/conf_out.ini
|
||||
ini.sublime-project
|
||||
ini.sublime-workspace
|
||||
testdata/conf_reflect.ini
|
||||
.idea
|
|
@ -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
|
|
@ -0,0 +1,9 @@
|
|||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
Icon?
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
.idea
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
*.db
|
|
@ -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.
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -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
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
redis-cli FLUSHALL
|
||||
if [ $? == "0" ];then
|
||||
go test -v -run=TestMysqlWithCache
|
||||
else
|
||||
echo "no redis-server running on localhost"
|
||||
fi
|
|
@ -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/test_mssql_cache.sh
generated
vendored
Executable file → Normal file
0
vendor/github.com/go-xorm/xorm/test_mssql_cache.sh
generated
vendored
Executable file → Normal file
0
vendor/github.com/go-xorm/xorm/test_mymysql_cache.sh
generated
vendored
Executable file → Normal file
0
vendor/github.com/go-xorm/xorm/test_mymysql_cache.sh
generated
vendored
Executable file → Normal file
0
vendor/github.com/go-xorm/xorm/test_mysql_cache.sh
generated
vendored
Executable file → Normal file
0
vendor/github.com/go-xorm/xorm/test_mysql_cache.sh
generated
vendored
Executable file → Normal file
0
vendor/github.com/go-xorm/xorm/test_postgres_cache.sh
generated
vendored
Executable file → Normal file
0
vendor/github.com/go-xorm/xorm/test_postgres_cache.sh
generated
vendored
Executable file → Normal file
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue