Stephen McQuay (smcquay)
479ef2b786
This allows godoc.org to work on nested packages. As it stood gddo would fail trying to find nested packages. Let's say we have a vaind with this route: go.mcquay.me/vain -> https://s.mcquay.me/sm/vain Asking godoc for go.mcquay.me/vain would work fine. However trying to get documentation for go.mcquay.me/vain/errors (nested package) would return 404 since it wouldn't match known paths exactly. Since now we match and return "go.mcquay.me/vain" for the full path, gddo is able to use local caches for its information. Change-Id: I599a75898493734fc652e507f477c11b1b1b13e8
261 lines
5.2 KiB
Go
261 lines
5.2 KiB
Go
package vain
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
verrors "mcquay.me/vain/errors"
|
|
"mcquay.me/vain/metrics"
|
|
)
|
|
|
|
// NewMemDB returns a functional MemDB.
|
|
func NewMemDB(p string) (*MemDB, error) {
|
|
m := &MemDB{
|
|
filename: p,
|
|
|
|
Users: map[Email]User{},
|
|
TokToEmail: map[Token]Email{},
|
|
|
|
Packages: map[path]Package{},
|
|
Namespaces: map[namespace]Email{},
|
|
}
|
|
|
|
f, err := os.Open(p)
|
|
if err != nil {
|
|
// file doesn't exist yet
|
|
return m, nil
|
|
}
|
|
err = json.NewDecoder(f).Decode(m)
|
|
return m, err
|
|
}
|
|
|
|
// MemDB implements an in-memory, and disk-backed database for a vain server.
|
|
type MemDB struct {
|
|
filename string
|
|
|
|
l sync.RWMutex
|
|
|
|
Users map[Email]User
|
|
TokToEmail map[Token]Email
|
|
|
|
Packages map[path]Package
|
|
Namespaces map[namespace]Email
|
|
}
|
|
|
|
// NSForToken creates an entry namespaces with a relation to the token.
|
|
func (m *MemDB) NSForToken(ns namespace, tok Token) error {
|
|
m.l.Lock()
|
|
defer m.l.Unlock()
|
|
|
|
e, ok := m.TokToEmail[tok]
|
|
if !ok {
|
|
return verrors.HTTP{
|
|
Message: fmt.Sprintf("User for token %q not found", tok),
|
|
Code: http.StatusNotFound,
|
|
}
|
|
}
|
|
|
|
if owner, ok := m.Namespaces[ns]; !ok {
|
|
m.Namespaces[ns] = e
|
|
} else {
|
|
if m.Namespaces[ns] != owner {
|
|
return verrors.HTTP{
|
|
Message: fmt.Sprintf("not authorized against namespace %q", ns),
|
|
Code: http.StatusUnauthorized,
|
|
}
|
|
}
|
|
}
|
|
return m.flush(m.filename)
|
|
}
|
|
|
|
// Package fetches the package associated with path.
|
|
func (m *MemDB) Package(pth string) (Package, error) {
|
|
m.l.RLock()
|
|
defer m.l.RUnlock()
|
|
|
|
pkg, ok := m.Packages[path(pth)]
|
|
if ok {
|
|
return pkg, nil
|
|
}
|
|
|
|
var longest Package
|
|
for _, p := range m.Packages {
|
|
if splitPathHasPrefix(strings.Split(pth, "/"), strings.Split(p.Path, "/")) {
|
|
if len(p.Path) > len(longest.Path) {
|
|
longest = p
|
|
}
|
|
}
|
|
}
|
|
|
|
var err error
|
|
if longest.Path == "" {
|
|
err = verrors.HTTP{
|
|
Message: fmt.Sprintf("couldn't find package %q", pth),
|
|
Code: http.StatusNotFound,
|
|
}
|
|
}
|
|
return longest, err
|
|
}
|
|
|
|
// AddPackage adds p into packages table.
|
|
func (m *MemDB) AddPackage(p Package) error {
|
|
m.l.Lock()
|
|
m.Packages[path(p.Path)] = p
|
|
m.l.Unlock()
|
|
return m.flush(m.filename)
|
|
}
|
|
|
|
// RemovePackage removes package with given path
|
|
func (m *MemDB) RemovePackage(pth path) error {
|
|
m.l.Lock()
|
|
delete(m.Packages, pth)
|
|
m.l.Unlock()
|
|
return m.flush(m.filename)
|
|
}
|
|
|
|
// PackageExists tells if a package with path is in the database.
|
|
func (m *MemDB) PackageExists(pth path) bool {
|
|
m.l.RLock()
|
|
_, ok := m.Packages[path(pth)]
|
|
m.l.RUnlock()
|
|
return ok
|
|
}
|
|
|
|
// Pkgs returns all packages from the database
|
|
func (m *MemDB) Pkgs() []Package {
|
|
ps := []Package{}
|
|
m.l.RLock()
|
|
for _, p := range m.Packages {
|
|
ps = append(ps, p)
|
|
}
|
|
m.l.RUnlock()
|
|
return ps
|
|
}
|
|
|
|
// Register adds email to the database, returning an error if there was one.
|
|
func (m *MemDB) Register(e Email) (Token, error) {
|
|
m.l.Lock()
|
|
defer m.l.Unlock()
|
|
|
|
if _, ok := m.Users[e]; ok {
|
|
return "", verrors.HTTP{
|
|
Message: fmt.Sprintf("duplicate email %q", e),
|
|
Code: http.StatusConflict,
|
|
}
|
|
}
|
|
|
|
tok := FreshToken()
|
|
m.Users[e] = User{
|
|
Email: e,
|
|
token: tok,
|
|
Requested: time.Now(),
|
|
}
|
|
m.TokToEmail[tok] = e
|
|
return tok, m.flush(m.filename)
|
|
}
|
|
|
|
// Confirm modifies the user with the given token. Used on register confirmation.
|
|
func (m *MemDB) Confirm(tok Token) (Token, error) {
|
|
m.l.Lock()
|
|
defer m.l.Unlock()
|
|
|
|
e, ok := m.TokToEmail[tok]
|
|
if !ok {
|
|
return "", verrors.HTTP{
|
|
Message: fmt.Sprintf("bad token: %s", tok),
|
|
Code: http.StatusNotFound,
|
|
}
|
|
}
|
|
|
|
delete(m.TokToEmail, tok)
|
|
tok = FreshToken()
|
|
u, ok := m.Users[e]
|
|
if !ok {
|
|
return "", verrors.HTTP{
|
|
Message: fmt.Sprintf("inconsistent db; found email for token %q, but no user for email %q", tok, e),
|
|
Code: http.StatusInternalServerError,
|
|
}
|
|
}
|
|
u.token = tok
|
|
m.Users[e] = u
|
|
m.TokToEmail[tok] = e
|
|
|
|
return tok, m.flush(m.filename)
|
|
}
|
|
|
|
// Forgot is used fetch a user's token. It implements rudimentary rate
|
|
// limiting.
|
|
func (m *MemDB) Forgot(e Email, window time.Duration) (Token, error) {
|
|
m.l.Lock()
|
|
defer m.l.Unlock()
|
|
|
|
u, ok := m.Users[e]
|
|
if !ok {
|
|
return "", verrors.HTTP{
|
|
Message: fmt.Sprintf("could not find email %q in db", e),
|
|
Code: http.StatusNotFound,
|
|
}
|
|
}
|
|
|
|
if u.Requested.After(time.Now()) {
|
|
return "", verrors.HTTP{
|
|
Message: fmt.Sprintf("rate limit hit for %q; try again in %0.2f mins", u.Email, u.Requested.Sub(time.Now()).Minutes()),
|
|
Code: http.StatusTooManyRequests,
|
|
}
|
|
}
|
|
|
|
return u.token, nil
|
|
}
|
|
|
|
// Sync takes a lock, and flushes the data to disk.
|
|
func (m *MemDB) Sync() error {
|
|
m.l.RLock()
|
|
defer m.l.RUnlock()
|
|
|
|
return m.flush(m.filename)
|
|
}
|
|
|
|
// flush writes to disk, but expects the user to have taken the lock.
|
|
func (m *MemDB) flush(p string) error {
|
|
defer metrics.DBTime("flush")()
|
|
f, err := os.Create(p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return json.NewEncoder(f).Encode(&m)
|
|
}
|
|
|
|
func (m *MemDB) addUser(e Email) (Token, error) {
|
|
tok := FreshToken()
|
|
|
|
m.l.Lock()
|
|
m.Users[e] = User{
|
|
Email: e,
|
|
token: tok,
|
|
Requested: time.Now(),
|
|
}
|
|
m.TokToEmail[tok] = e
|
|
m.l.Unlock()
|
|
|
|
return tok, m.flush(m.filename)
|
|
}
|
|
|
|
func (m *MemDB) user(e Email) (User, error) {
|
|
m.l.Lock()
|
|
u, ok := m.Users[e]
|
|
m.l.Unlock()
|
|
var err error
|
|
if !ok {
|
|
err = verrors.HTTP{
|
|
Message: fmt.Sprintf("couldn't find user %q", e),
|
|
Code: http.StatusNotFound,
|
|
}
|
|
}
|
|
return u, err
|
|
}
|