caddy/vendor/github.com/mholt/certmagic/filestoragesync.go

146 lines
3.8 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"
"os"
"path/filepath"
"sync"
"time"
)
// FileStorageLocker implements the Locker interface
// using the file system. An empty value is NOT VALID,
// so you must use NewFileStorageLocker() to get one.
type FileStorageLocker struct {
fs FileStorage
}
// NewFileStorageLocker returns a valid Locker backed by fs.
func NewFileStorageLocker(fs FileStorage) *FileStorageLocker {
return &FileStorageLocker{fs: fs}
}
// TryLock attempts to get a lock for name, otherwise it returns
// a Waiter value to wait until the other process is finished.
func (l *FileStorageLocker) TryLock(name string) (Waiter, error) {
fileStorageNameLocksMu.Lock()
defer fileStorageNameLocksMu.Unlock()
// see if lock already exists within this process
fw, ok := fileStorageNameLocks[name]
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 := l.lockDir()
if err := os.MkdirAll(lockDir, 0700); err != nil {
return nil, err
}
fw = &FileStorageWaiter{
filename: filepath.Join(lockDir, safeKey(name)+".lock"),
wg: new(sync.WaitGroup),
}
// 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; use it to wait
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[name] = fw
return nil, nil
}
// Unlock releases the lock for name.
func (l *FileStorageLocker) Unlock(name string) error {
fileStorageNameLocksMu.Lock()
defer fileStorageNameLocksMu.Unlock()
fw, ok := fileStorageNameLocks[name]
if !ok {
return fmt.Errorf("FileStorageLocker: no lock to release for %s", name)
}
// remove lock file
os.Remove(fw.filename)
// if parent folder is now empty, remove it too to keep it tidy
dir, err := os.Open(l.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, name)
return nil
}
func (l *FileStorageLocker) lockDir() string {
return filepath.Join(l.fs.Path, "locks")
}
// 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 poll intervals to get their answer.)
type FileStorageWaiter struct {
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 _ Locker = &FileStorageLocker{}
var _ Waiter = &FileStorageWaiter{}