dispatch/vendor/github.com/mholt/certmagic/filestorage.go

309 lines
7.7 KiB
Go

// Copyright 2015 Matthew Holt
//
// 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 certmagic
import (
"fmt"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"runtime"
"sync"
"time"
)
// FileStorage facilitates forming file paths derived from a root
// directory. It is used to get file paths in a consistent,
// cross-platform way or persisting ACME assets on the file system.
type FileStorage struct {
Path string
}
// Exists returns true if key exists in fs.
func (fs FileStorage) Exists(key string) bool {
_, err := os.Stat(fs.Filename(key))
return !os.IsNotExist(err)
}
// Store saves value at key.
func (fs FileStorage) Store(key string, value []byte) error {
filename := fs.Filename(key)
err := os.MkdirAll(filepath.Dir(filename), 0700)
if err != nil {
return err
}
return ioutil.WriteFile(filename, value, 0600)
}
// Load retrieves the value at key.
func (fs FileStorage) Load(key string) ([]byte, error) {
contents, err := ioutil.ReadFile(fs.Filename(key))
if os.IsNotExist(err) {
return nil, ErrNotExist(err)
}
return contents, nil
}
// Delete deletes the value at key.
// TODO: Delete any empty folders caused by this operation
func (fs FileStorage) Delete(key string) error {
err := os.Remove(fs.Filename(key))
if os.IsNotExist(err) {
return ErrNotExist(err)
}
return err
}
// List returns all keys that match prefix.
func (fs FileStorage) List(prefix string, recursive bool) ([]string, error) {
var keys []string
walkPrefix := fs.Filename(prefix)
err := filepath.Walk(walkPrefix, func(fpath string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info == nil {
return fmt.Errorf("%s: file info is nil", fpath)
}
if fpath == walkPrefix {
return nil
}
suffix, err := filepath.Rel(walkPrefix, fpath)
if err != nil {
return fmt.Errorf("%s: could not make path relative: %v", fpath, err)
}
keys = append(keys, path.Join(prefix, suffix))
if !recursive && info.IsDir() {
return filepath.SkipDir
}
return nil
})
return keys, err
}
// Stat returns information about key.
func (fs FileStorage) Stat(key string) (KeyInfo, error) {
fi, err := os.Stat(fs.Filename(key))
if os.IsNotExist(err) {
return KeyInfo{}, ErrNotExist(err)
}
if err != nil {
return KeyInfo{}, err
}
return KeyInfo{
Key: key,
Modified: fi.ModTime(),
Size: fi.Size(),
IsTerminal: !fi.IsDir(),
}, nil
}
// Filename returns the key as a path on the file
// system prefixed by fs.Path.
func (fs FileStorage) Filename(key string) string {
return filepath.Join(fs.Path, filepath.FromSlash(key))
}
// homeDir returns the best guess of the current user's home
// directory from environment variables. If unknown, "." (the
// current directory) is returned instead.
func homeDir() string {
home := os.Getenv("HOME")
if home == "" && runtime.GOOS == "windows" {
drive := os.Getenv("HOMEDRIVE")
path := os.Getenv("HOMEPATH")
home = drive + path
if drive == "" || path == "" {
home = os.Getenv("USERPROFILE")
}
}
if home == "" {
home = "."
}
return home
}
func dataDir() string {
baseDir := filepath.Join(homeDir(), ".local", "share")
if xdgData := os.Getenv("XDG_DATA_HOME"); xdgData != "" {
baseDir = xdgData
}
return filepath.Join(baseDir, "certmagic")
}
// TryLock attempts to get a lock for name, otherwise it returns
// a Waiter value to wait until the other process is finished.
func (fs FileStorage) TryLock(key string) (Waiter, error) {
fileStorageNameLocksMu.Lock()
defer fileStorageNameLocksMu.Unlock()
// see if lock already exists within this process - allows
// for faster unlocking since we don't have to poll the disk
fw, ok := fileStorageNameLocks[key]
if ok {
// lock already created within process, let caller wait on it
return fw, nil
}
// attempt to persist lock to disk by creating lock file
// parent dir must exist
lockDir := fs.lockDir()
if err := os.MkdirAll(lockDir, 0700); err != nil {
return nil, err
}
fw = &fileStorageWaiter{
key: key,
filename: filepath.Join(lockDir, StorageKeys.safe(key)+".lock"),
wg: new(sync.WaitGroup),
}
var checkedStaleLock bool // sentinel value to avoid infinite goto-ing
createLock:
// create the file in a special mode such that an
// error is returned if it already exists
lf, err := os.OpenFile(fw.filename, os.O_CREATE|os.O_EXCL, 0644)
if err != nil {
if os.IsExist(err) {
// another process has the lock
// check to see if the lock is stale, if we haven't already
if !checkedStaleLock {
checkedStaleLock = true
if fs.lockFileStale(fw.filename) {
log.Printf("[INFO][%s] Lock for '%s' is stale; removing then retrying: %s",
fs, key, fw.filename)
os.Remove(fw.filename)
goto createLock
}
}
// if lock is not stale, wait upon it
return fw, nil
}
// otherwise, this was some unexpected error
return nil, err
}
lf.Close()
// looks like we get the lock
fw.wg.Add(1)
fileStorageNameLocks[key] = fw
return nil, nil
}
// Unlock releases the lock for name.
func (fs FileStorage) Unlock(key string) error {
fileStorageNameLocksMu.Lock()
defer fileStorageNameLocksMu.Unlock()
fw, ok := fileStorageNameLocks[key]
if !ok {
return fmt.Errorf("FileStorage: no lock to release for %s", key)
}
// remove lock file
os.Remove(fw.filename)
// if parent folder is now empty, remove it too to keep it tidy
dir, err := os.Open(fs.lockDir()) // OK to ignore error here
if err == nil {
items, _ := dir.Readdirnames(3) // OK to ignore error here
if len(items) == 0 {
os.Remove(dir.Name())
}
dir.Close()
}
// clean up in memory
fw.wg.Done()
delete(fileStorageNameLocks, key)
return nil
}
// UnlockAllObtained removes all locks obtained by
// this instance of fs.
func (fs FileStorage) UnlockAllObtained() {
for key, fw := range fileStorageNameLocks {
err := fs.Unlock(fw.key)
if err != nil {
log.Printf("[ERROR][%s] Releasing obtained lock for %s: %v", fs, key, err)
}
}
}
func (fs FileStorage) lockFileStale(filename string) bool {
info, err := os.Stat(filename)
if err != nil {
return true // no good way to handle this, really; lock is useless?
}
return time.Since(info.ModTime()) > staleLockDuration
}
func (fs FileStorage) lockDir() string {
return filepath.Join(fs.Path, "locks")
}
func (fs FileStorage) String() string {
return "FileStorage:" + fs.Path
}
// fileStorageWaiter waits for a file to disappear; it
// polls the file system to check for the existence of
// a file. It also uses a WaitGroup to optimize the
// polling in the case when this process is the only
// one waiting. (Other processes that are waiting for
// the lock will still block, but must wait for the
// polling to get their answer.)
type fileStorageWaiter struct {
key string
filename string
wg *sync.WaitGroup
}
// Wait waits until the lock is released.
func (fw *fileStorageWaiter) Wait() {
start := time.Now()
fw.wg.Wait()
for time.Since(start) < 1*time.Hour {
_, err := os.Stat(fw.filename)
if os.IsNotExist(err) {
return
}
time.Sleep(1 * time.Second)
}
}
var fileStorageNameLocks = make(map[string]*fileStorageWaiter)
var fileStorageNameLocksMu sync.Mutex
var _ Storage = FileStorage{}
var _ Waiter = &fileStorageWaiter{}
// staleLockDuration is the length of time
// before considering a lock to be stale.
const staleLockDuration = 2 * time.Hour