mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-17 17:47:03 +00:00
storage: $ can be used in place of : in filestore path (#449)
This commit is contained in:
@@ -442,7 +442,8 @@ separated list of key:value pairs.
|
|||||||
#### `file` type parameters
|
#### `file` type parameters
|
||||||
|
|
||||||
- `path`: Operating system specific path to the directory where mail should be
|
- `path`: Operating system specific path to the directory where mail should be
|
||||||
stored.
|
stored. `$` characters will be replaced with `:` in the final path value,
|
||||||
|
allowing Windows drive letters, i.e. `D$\inbucket`.
|
||||||
|
|
||||||
#### `memory` type parameters
|
#### `memory` type parameters
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ const indexFileName = "index.gob"
|
|||||||
var (
|
var (
|
||||||
// countChannel is filled with a sequential numbers (0000..9999), which are
|
// countChannel is filled with a sequential numbers (0000..9999), which are
|
||||||
// used by generateID() to generate unique message IDs. It's global
|
// used by generateID() to generate unique message IDs. It's global
|
||||||
// because we only want one regardless of the number of DataStore objects
|
// because we only want one regardless of the number of DataStore objects.
|
||||||
countChannel = make(chan int, 10)
|
countChannel = make(chan int, 10)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ func init() {
|
|||||||
go countGenerator(countChannel)
|
go countGenerator(countChannel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populates the channel with numbers
|
// Populates the channel with numbers.
|
||||||
func countGenerator(c chan int) {
|
func countGenerator(c chan int) {
|
||||||
for i := 0; true; i = (i + 1) % 10000 {
|
for i := 0; true; i = (i + 1) % 10000 {
|
||||||
c <- i
|
c <- i
|
||||||
@@ -40,7 +41,7 @@ func countGenerator(c chan int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Store implements DataStore aand is the root of the mail storage
|
// Store implements DataStore aand is the root of the mail storage
|
||||||
// hiearchy. It provides access to Mailbox objects
|
// hiearchy. It provides access to Mailbox objects.
|
||||||
type Store struct {
|
type Store struct {
|
||||||
hashLock storage.HashLock
|
hashLock storage.HashLock
|
||||||
path string
|
path string
|
||||||
@@ -50,13 +51,14 @@ type Store struct {
|
|||||||
extHost *extension.Host
|
extHost *extension.Host
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new DataStore object using the specified path
|
// New creates a new DataStore object using the specified path.
|
||||||
func New(cfg config.Storage, extHost *extension.Host) (storage.Store, error) {
|
func New(cfg config.Storage, extHost *extension.Host) (storage.Store, error) {
|
||||||
path := cfg.Params["path"]
|
path := cfg.Params["path"]
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return nil, fmt.Errorf("'path' parameter not specified")
|
return nil, fmt.Errorf("'path' parameter not specified")
|
||||||
}
|
}
|
||||||
mailPath := filepath.Join(path, "mail")
|
|
||||||
|
mailPath := getMailPath(path)
|
||||||
if _, err := os.Stat(mailPath); err != nil {
|
if _, err := os.Stat(mailPath); err != nil {
|
||||||
// Mail datastore does not yet exist, create it.
|
// Mail datastore does not yet exist, create it.
|
||||||
if err = os.MkdirAll(mailPath, 0770); err != nil {
|
if err = os.MkdirAll(mailPath, 0770); err != nil {
|
||||||
@@ -88,16 +90,19 @@ func (fs *Store) AddMessage(m storage.Message) (id string, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new message.
|
// Create a new message.
|
||||||
fm, err := mb.newMessage()
|
fm, err := mb.newMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure mailbox directory exists.
|
// Ensure mailbox directory exists.
|
||||||
if err := mb.createDir(); err != nil {
|
if err := mb.createDir(); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
// Write the message content
|
|
||||||
|
// Write the message content.
|
||||||
file, err := os.Create(fm.rawPath())
|
file, err := os.Create(fm.rawPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -105,23 +110,24 @@ func (fs *Store) AddMessage(m storage.Message) (id string, err error) {
|
|||||||
w := bufio.NewWriter(file)
|
w := bufio.NewWriter(file)
|
||||||
size, err := io.Copy(w, r)
|
size, err := io.Copy(w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Try to remove the file
|
// Try to remove the file.
|
||||||
_ = file.Close()
|
_ = file.Close()
|
||||||
_ = os.Remove(fm.rawPath())
|
_ = os.Remove(fm.rawPath())
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
_ = r.Close()
|
_ = r.Close()
|
||||||
if err := w.Flush(); err != nil {
|
if err := w.Flush(); err != nil {
|
||||||
// Try to remove the file
|
// Try to remove the file.
|
||||||
_ = file.Close()
|
_ = file.Close()
|
||||||
_ = os.Remove(fm.rawPath())
|
_ = os.Remove(fm.rawPath())
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if err := file.Close(); err != nil {
|
if err := file.Close(); err != nil {
|
||||||
// Try to remove the file
|
// Try to remove the file.
|
||||||
_ = os.Remove(fm.rawPath())
|
_ = os.Remove(fm.rawPath())
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the index.
|
// Update the index.
|
||||||
fm.Fdate = m.Date()
|
fm.Fdate = m.Date()
|
||||||
fm.Ffrom = m.From()
|
fm.Ffrom = m.From()
|
||||||
@@ -130,10 +136,11 @@ func (fs *Store) AddMessage(m storage.Message) (id string, err error) {
|
|||||||
fm.Fsubject = m.Subject()
|
fm.Fsubject = m.Subject()
|
||||||
mb.messages = append(mb.messages, fm)
|
mb.messages = append(mb.messages, fm)
|
||||||
if err := mb.writeIndex(); err != nil {
|
if err := mb.writeIndex(); err != nil {
|
||||||
// Try to remove the file
|
// Try to remove the file.
|
||||||
_ = os.Remove(fm.rawPath())
|
_ = os.Remove(fm.rawPath())
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return fm.Fid, nil
|
return fm.Fid, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,11 +165,13 @@ func (fs *Store) MarkSeen(mailbox, id string) error {
|
|||||||
mb := fs.mbox(mailbox)
|
mb := fs.mbox(mailbox)
|
||||||
mb.Lock()
|
mb.Lock()
|
||||||
defer mb.Unlock()
|
defer mb.Unlock()
|
||||||
|
|
||||||
if !mb.indexLoaded {
|
if !mb.indexLoaded {
|
||||||
if err := mb.readIndex(); err != nil {
|
if err := mb.readIndex(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, m := range mb.messages {
|
for _, m := range mb.messages {
|
||||||
if m.Fid == id {
|
if m.Fid == id {
|
||||||
if m.Fseen {
|
if m.Fseen {
|
||||||
@@ -173,6 +182,7 @@ func (fs *Store) MarkSeen(mailbox, id string) error {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return mb.writeIndex()
|
return mb.writeIndex()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,19 +220,22 @@ func (fs *Store) VisitMailboxes(f func([]storage.Message) (cont bool)) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Loop over level 1 directories
|
|
||||||
|
// Loop over level 1 directories.
|
||||||
for _, name1 := range names1 {
|
for _, name1 := range names1 {
|
||||||
names2, err := readDirNames(fs.mailPath, name1)
|
names2, err := readDirNames(fs.mailPath, name1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Loop over level 2 directories
|
|
||||||
|
// Loop over level 2 directories.
|
||||||
for _, name2 := range names2 {
|
for _, name2 := range names2 {
|
||||||
names3, err := readDirNames(fs.mailPath, name1, name2)
|
names3, err := readDirNames(fs.mailPath, name1, name2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Loop over mailboxes
|
|
||||||
|
// Loop over mailboxes.
|
||||||
for _, name3 := range names3 {
|
for _, name3 := range names3 {
|
||||||
mb := fs.mboxFromHash(name3)
|
mb := fs.mboxFromHash(name3)
|
||||||
mb.RLock()
|
mb.RLock()
|
||||||
@@ -247,6 +260,7 @@ func (fs *Store) mbox(mailbox string) *mbox {
|
|||||||
s2 := hash[0:6]
|
s2 := hash[0:6]
|
||||||
path := filepath.Join(fs.mailPath, s1, s2, hash)
|
path := filepath.Join(fs.mailPath, s1, s2, hash)
|
||||||
indexPath := filepath.Join(path, indexFileName)
|
indexPath := filepath.Join(path, indexFileName)
|
||||||
|
|
||||||
return &mbox{
|
return &mbox{
|
||||||
RWMutex: fs.hashLock.Get(hash),
|
RWMutex: fs.hashLock.Get(hash),
|
||||||
store: fs,
|
store: fs,
|
||||||
@@ -263,6 +277,7 @@ func (fs *Store) mboxFromHash(hash string) *mbox {
|
|||||||
s2 := hash[0:6]
|
s2 := hash[0:6]
|
||||||
path := filepath.Join(fs.mailPath, s1, s2, hash)
|
path := filepath.Join(fs.mailPath, s1, s2, hash)
|
||||||
indexPath := filepath.Join(path, indexFileName)
|
indexPath := filepath.Join(path, indexFileName)
|
||||||
|
|
||||||
return &mbox{
|
return &mbox{
|
||||||
RWMutex: fs.hashLock.Get(hash),
|
RWMutex: fs.hashLock.Get(hash),
|
||||||
store: fs,
|
store: fs,
|
||||||
@@ -297,6 +312,14 @@ func generateID(date time.Time) string {
|
|||||||
return generatePrefix(date) + "-" + fmt.Sprintf("%04d", <-countChannel)
|
return generatePrefix(date) + "-" + fmt.Sprintf("%04d", <-countChannel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getMailPath converts a filestore `path` parameter into the effective mail store path.
|
||||||
|
// Within the path, '$' is replaced with ':' to support Windows drive letters with our
|
||||||
|
// env->config map syntax.
|
||||||
|
func getMailPath(base string) string {
|
||||||
|
path := strings.ReplaceAll(base, "$", ":")
|
||||||
|
return filepath.Join(path, "mail")
|
||||||
|
}
|
||||||
|
|
||||||
// readDirNames returns a slice of filenames in the specified directory or an error.
|
// readDirNames returns a slice of filenames in the specified directory or an error.
|
||||||
func readDirNames(elem ...string) ([]string, error) {
|
func readDirNames(elem ...string) ([]string, error) {
|
||||||
f, err := os.Open(filepath.Join(elem...))
|
f, err := os.Open(filepath.Join(elem...))
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/inbucket/inbucket/v3/pkg/storage"
|
"github.com/inbucket/inbucket/v3/pkg/storage"
|
||||||
"github.com/inbucket/inbucket/v3/pkg/test"
|
"github.com/inbucket/inbucket/v3/pkg/test"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestSuite runs storage package test suite on file store.
|
// TestSuite runs storage package test suite on file store.
|
||||||
@@ -33,6 +34,24 @@ func TestSuite(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test filestore initialization.
|
||||||
|
func TestFSNew(t *testing.T) {
|
||||||
|
// Should fail if no path specified.
|
||||||
|
ds, err := New(config.Storage{}, extension.NewHost())
|
||||||
|
assert.ErrorContains(t, err, "parameter not specified")
|
||||||
|
assert.Nil(t, ds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFSGetMailPath(t *testing.T) {
|
||||||
|
// Path should have `mail` dir appended.
|
||||||
|
got := getMailPath(`one`)
|
||||||
|
assert.Regexp(t, "^one.mail$", got, "Expected one/mail or similar")
|
||||||
|
|
||||||
|
// Path should convert `$` to `:`.
|
||||||
|
got = getMailPath(`C$\inbucket`)
|
||||||
|
assert.Regexp(t, "^C:.inbucket.mail$", got, "Expected C:\\inbucket\\mail or similar")
|
||||||
|
}
|
||||||
|
|
||||||
// Test directory structure created by filestore
|
// Test directory structure created by filestore
|
||||||
func TestFSDirStructure(t *testing.T) {
|
func TestFSDirStructure(t *testing.T) {
|
||||||
ds, logbuf := setupDataStore(config.Storage{}, extension.NewHost())
|
ds, logbuf := setupDataStore(config.Storage{}, extension.NewHost())
|
||||||
@@ -167,14 +186,14 @@ func TestGetLatestMessage(t *testing.T) {
|
|||||||
|
|
||||||
// Test get the latest message
|
// Test get the latest message
|
||||||
msg, err = ds.GetMessage(mbName, "latest")
|
msg, err = ds.GetMessage(mbName, "latest")
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
assert.True(t, msg.ID() == id2, "Expected %q to be equal to %q", msg.ID(), id2)
|
assert.True(t, msg.ID() == id2, "Expected %q to be equal to %q", msg.ID(), id2)
|
||||||
|
|
||||||
// Deliver test message 3
|
// Deliver test message 3
|
||||||
id3, _ := deliverMessage(ds, mbName, "test 3", time.Now())
|
id3, _ := deliverMessage(ds, mbName, "test 3", time.Now())
|
||||||
|
|
||||||
msg, err = ds.GetMessage(mbName, "latest")
|
msg, err = ds.GetMessage(mbName, "latest")
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
assert.True(t, msg.ID() == id3, "Expected %q to be equal to %q", msg.ID(), id3)
|
assert.True(t, msg.ID() == id3, "Expected %q to be equal to %q", msg.ID(), id3)
|
||||||
|
|
||||||
// Test wrong id
|
// Test wrong id
|
||||||
|
|||||||
Reference in New Issue
Block a user