storage: fs: implement read-only mode (closes #111)
diff --git a/leveldb/bench_test.go b/leveldb/bench_test.go
index 91b4267..5c587b9 100644
--- a/leveldb/bench_test.go
+++ b/leveldb/bench_test.go
@@ -90,7 +90,7 @@
 		ro: &opt.ReadOptions{},
 		wo: &opt.WriteOptions{},
 	}
-	p.stor, err = storage.OpenFile(benchDB)
+	p.stor, err = storage.OpenFile(benchDB, false)
 	if err != nil {
 		b.Fatal("cannot open stor: ", err)
 	}
diff --git a/leveldb/db.go b/leveldb/db.go
index b81baf7..537addb 100644
--- a/leveldb/db.go
+++ b/leveldb/db.go
@@ -211,7 +211,7 @@
 // The returned DB instance is goroutine-safe.
 // The DB must be closed after use, by calling Close method.
 func OpenFile(path string, o *opt.Options) (db *DB, err error) {
-	stor, err := storage.OpenFile(path)
+	stor, err := storage.OpenFile(path, o.GetReadOnly())
 	if err != nil {
 		return
 	}
@@ -261,7 +261,7 @@
 // The returned DB instance is goroutine-safe.
 // The DB must be closed after use, by calling Close method.
 func RecoverFile(path string, o *opt.Options) (db *DB, err error) {
-	stor, err := storage.OpenFile(path)
+	stor, err := storage.OpenFile(path, false)
 	if err != nil {
 		return
 	}
diff --git a/leveldb/db_test.go b/leveldb/db_test.go
index e37cbc9..63592c0 100644
--- a/leveldb/db_test.go
+++ b/leveldb/db_test.go
@@ -1877,7 +1877,7 @@
 	defer os.RemoveAll(dbpath)
 
 	for i := 0; i < 3; i++ {
-		stor, err := storage.OpenFile(dbpath)
+		stor, err := storage.OpenFile(dbpath, false)
 		if err != nil {
 			t.Fatalf("(%d) cannot open storage: %s", i, err)
 		}
diff --git a/leveldb/storage/file_storage.go b/leveldb/storage/file_storage.go
index e70eb64..cbe1dc1 100644
--- a/leveldb/storage/file_storage.go
+++ b/leveldb/storage/file_storage.go
@@ -19,7 +19,10 @@
 	"time"
 )
 
-var errFileOpen = errors.New("leveldb/storage: file still open")
+var (
+	errFileOpen = errors.New("leveldb/storage: file still open")
+	errReadOnly = errors.New("leveldb/storage: storage is read-only")
+)
 
 type fileLock interface {
 	release() error
@@ -30,26 +33,27 @@
 }
 
 func (lock *fileStorageLock) Release() {
-	fs := lock.fs
-	fs.mu.Lock()
-	defer fs.mu.Unlock()
-	if fs.slock == lock {
-		fs.slock = nil
+	if lock.fs != nil {
+		lock.fs.mu.Lock()
+		defer lock.fs.mu.Unlock()
+		if lock.fs.slock == lock {
+			lock.fs.slock = nil
+		}
 	}
-	return
 }
 
 const logSizeThreshold = 1024 * 1024 // 1 MiB
 
 // fileStorage is a file-system backed storage.
 type fileStorage struct {
-	path string
+	path     string
+	readOnly bool
 
 	mu      sync.Mutex
 	flock   fileLock
 	slock   *fileStorageLock
 	logw    *os.File
-	logSize int
+	logSize int64
 	buf     []byte
 	// Opened file counter; if open < 0 means closed.
 	open int
@@ -61,12 +65,20 @@
 // same path will fail.
 //
 // The storage must be closed after use, by calling Close method.
-func OpenFile(path string) (Storage, error) {
-	if err := os.MkdirAll(path, 0755); err != nil {
+func OpenFile(path string, readOnly bool) (Storage, error) {
+	if fi, err := os.Stat(path); err == nil {
+		if !fi.IsDir() {
+			return nil, fmt.Errorf("leveldb/storage: open %s: not a directory", path)
+		}
+	} else if os.IsNotExist(err) && !readOnly {
+		if err := os.MkdirAll(path, 0755); err != nil {
+			return nil, err
+		}
+	} else {
 		return nil, err
 	}
 
-	flock, err := newFileLock(filepath.Join(path, "LOCK"))
+	flock, err := newFileLock(filepath.Join(path, "LOCK"), readOnly)
 	if err != nil {
 		return nil, err
 	}
@@ -77,17 +89,29 @@
 		}
 	}()
 
-	logw, err := os.OpenFile(filepath.Join(path, "LOG"), os.O_WRONLY|os.O_CREATE, 0644)
-	if err != nil {
-		return nil, err
-	}
-	logSize, err := logw.Seek(0, os.SEEK_END)
-	if err != nil {
-		logw.Close()
-		return nil, err
+	var (
+		logw    *os.File
+		logSize int64
+	)
+	if !readOnly {
+		logw, err = os.OpenFile(filepath.Join(path, "LOG"), os.O_WRONLY|os.O_CREATE, 0644)
+		if err != nil {
+			return nil, err
+		}
+		logSize, err = logw.Seek(0, os.SEEK_END)
+		if err != nil {
+			logw.Close()
+			return nil, err
+		}
 	}
 
-	fs := &fileStorage{path: path, flock: flock, logw: logw, logSize: int(logSize)}
+	fs := &fileStorage{
+		path:     path,
+		readOnly: readOnly,
+		flock:    flock,
+		logw:     logw,
+		logSize:  logSize,
+	}
 	runtime.SetFinalizer(fs, (*fileStorage).Close)
 	return fs, nil
 }
@@ -98,6 +122,9 @@
 	if fs.open < 0 {
 		return nil, ErrClosed
 	}
+	if fs.readOnly {
+		return &fileStorageLock{}, nil
+	}
 	if fs.slock != nil {
 		return nil, ErrLocked
 	}
@@ -106,7 +133,7 @@
 }
 
 func itoa(buf []byte, i int, wid int) []byte {
-	var u uint = uint(i)
+	u := uint(i)
 	if u == 0 && wid <= 1 {
 		return append(buf, '0')
 	}
@@ -166,23 +193,30 @@
 }
 
 func (fs *fileStorage) Log(str string) {
-	t := time.Now()
-	fs.mu.Lock()
-	defer fs.mu.Unlock()
-	if fs.open < 0 {
-		return
+	if !fs.readOnly {
+		t := time.Now()
+		fs.mu.Lock()
+		defer fs.mu.Unlock()
+		if fs.open < 0 {
+			return
+		}
+		fs.doLog(t, str)
 	}
-	fs.doLog(t, str)
 }
 
 func (fs *fileStorage) log(str string) {
-	fs.doLog(time.Now(), str)
+	if !fs.readOnly {
+		fs.doLog(time.Now(), str)
+	}
 }
 
 func (fs *fileStorage) SetMeta(fd FileDesc) (err error) {
 	if !FileDescOk(fd) {
 		return ErrInvalidFile
 	}
+	if fs.readOnly {
+		return errReadOnly
+	}
 
 	fs.mu.Lock()
 	defer fs.mu.Unlock()
@@ -297,18 +331,20 @@
 		}
 		return
 	}
-	// Rename pending CURRENT file to an effective CURRENT.
-	if pend {
-		path := fmt.Sprintf("%s.%d", filepath.Join(fs.path, "CURRENT"), fd.Num)
-		if err := rename(path, filepath.Join(fs.path, "CURRENT")); err != nil {
-			fs.log(fmt.Sprintf("CURRENT.%d -> CURRENT: %v", fd.Num, err))
+	if !fs.readOnly {
+		// Rename pending CURRENT file to an effective CURRENT.
+		if pend {
+			path := fmt.Sprintf("%s.%d", filepath.Join(fs.path, "CURRENT"), fd.Num)
+			if err := rename(path, filepath.Join(fs.path, "CURRENT")); err != nil {
+				fs.log(fmt.Sprintf("CURRENT.%d -> CURRENT: %v", fd.Num, err))
+			}
 		}
-	}
-	// Remove obsolete or incomplete pending CURRENT files.
-	for _, name := range rem {
-		path := filepath.Join(fs.path, name)
-		if err := os.Remove(path); err != nil {
-			fs.log(fmt.Sprintf("remove %s: %v", name, err))
+		// Remove obsolete or incomplete pending CURRENT files.
+		for _, name := range rem {
+			path := filepath.Join(fs.path, name)
+			if err := os.Remove(path); err != nil {
+				fs.log(fmt.Sprintf("remove %s: %v", name, err))
+			}
 		}
 	}
 	return
@@ -368,6 +404,9 @@
 	if !FileDescOk(fd) {
 		return nil, ErrInvalidFile
 	}
+	if fs.readOnly {
+		return nil, errReadOnly
+	}
 
 	fs.mu.Lock()
 	defer fs.mu.Unlock()
@@ -386,6 +425,9 @@
 	if !FileDescOk(fd) {
 		return ErrInvalidFile
 	}
+	if fs.readOnly {
+		return errReadOnly
+	}
 
 	fs.mu.Lock()
 	defer fs.mu.Unlock()
@@ -413,6 +455,9 @@
 	if oldfd == newfd {
 		return nil
 	}
+	if fs.readOnly {
+		return errReadOnly
+	}
 
 	fs.mu.Lock()
 	defer fs.mu.Unlock()
diff --git a/leveldb/storage/file_storage_plan9.go b/leveldb/storage/file_storage_plan9.go
index 42940d7..bab62bf 100644
--- a/leveldb/storage/file_storage_plan9.go
+++ b/leveldb/storage/file_storage_plan9.go
@@ -19,8 +19,21 @@
 	return fl.f.Close()
 }
 
-func newFileLock(path string) (fl fileLock, err error) {
-	f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, os.ModeExclusive|0644)
+func newFileLock(path string, readOnly bool) (fl fileLock, err error) {
+	var (
+		flag int
+		perm os.FileMode
+	)
+	if readOnly {
+		flag = os.O_RDONLY
+	} else {
+		flag = os.O_RDWR
+		perm = os.ModeExclusive
+	}
+	f, err := os.OpenFile(path, flag, perm)
+	if os.IsNotExist(err) {
+		f, err = os.OpenFile(path, flag|os.O_CREATE, perm|0644)
+	}
 	if err != nil {
 		return
 	}
diff --git a/leveldb/storage/file_storage_solaris.go b/leveldb/storage/file_storage_solaris.go
index 102031b..79901ee 100644
--- a/leveldb/storage/file_storage_solaris.go
+++ b/leveldb/storage/file_storage_solaris.go
@@ -18,18 +18,27 @@
 }
 
 func (fl *unixFileLock) release() error {
-	if err := setFileLock(fl.f, false); err != nil {
+	if err := setFileLock(fl.f, false, false); err != nil {
 		return err
 	}
 	return fl.f.Close()
 }
 
-func newFileLock(path string) (fl fileLock, err error) {
-	f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)
+func newFileLock(path string, readOnly bool) (fl fileLock, err error) {
+	var flag int
+	if readOnly {
+		flag = os.O_RDONLY
+	} else {
+		flag = os.O_RDWR
+	}
+	f, err := os.OpenFile(path, flag, 0)
+	if os.IsNotExist(err) {
+		f, err = os.OpenFile(path, flag|os.O_CREATE, 0644)
+	}
 	if err != nil {
 		return
 	}
-	err = setFileLock(f, true)
+	err = setFileLock(f, readOnly, true)
 	if err != nil {
 		f.Close()
 		return
@@ -38,7 +47,7 @@
 	return
 }
 
-func setFileLock(f *os.File, lock bool) error {
+func setFileLock(f *os.File, readOnly, lock bool) error {
 	flock := syscall.Flock_t{
 		Type:   syscall.F_UNLCK,
 		Start:  0,
@@ -46,7 +55,11 @@
 		Whence: 1,
 	}
 	if lock {
-		flock.Type = syscall.F_WRLCK
+		if readOnly {
+			flock.Type = syscall.F_RDLCK
+		} else {
+			flock.Type = syscall.F_WRLCK
+		}
 	}
 	return syscall.FcntlFlock(f.Fd(), syscall.F_SETLK, &flock)
 }
diff --git a/leveldb/storage/file_storage_test.go b/leveldb/storage/file_storage_test.go
index f88011c..7a77f28 100644
--- a/leveldb/storage/file_storage_test.go
+++ b/leveldb/storage/file_storage_test.go
@@ -88,24 +88,18 @@
 }
 
 func TestFileStorage_Locking(t *testing.T) {
-	path := filepath.Join(os.TempDir(), fmt.Sprintf("goleveldbtestfd-%d", os.Getuid()))
-
-	_, err := os.Stat(path)
-	if err == nil {
-		err = os.RemoveAll(path)
-		if err != nil {
-			t.Fatal("RemoveAll: got error: ", err)
-		}
+	path := filepath.Join(os.TempDir(), fmt.Sprintf("goleveldb-testrwlock-%d", os.Getuid()))
+	if err := os.RemoveAll(path); err != nil && !os.IsNotExist(err) {
+		t.Fatal("RemoveAll: got error: ", err)
 	}
+	defer os.RemoveAll(path)
 
-	p1, err := OpenFile(path)
+	p1, err := OpenFile(path, false)
 	if err != nil {
 		t.Fatal("OpenFile(1): got error: ", err)
 	}
 
-	defer os.RemoveAll(path)
-
-	p2, err := OpenFile(path)
+	p2, err := OpenFile(path, false)
 	if err != nil {
 		t.Logf("OpenFile(2): got error: %s (expected)", err)
 	} else {
@@ -116,7 +110,7 @@
 
 	p1.Close()
 
-	p3, err := OpenFile(path)
+	p3, err := OpenFile(path, false)
 	if err != nil {
 		t.Fatal("OpenFile(3): got error: ", err)
 	}
@@ -138,3 +132,45 @@
 		t.Fatal("storage lock failed(2): ", err)
 	}
 }
+
+func TestFileStorage_ReadOnlyLocking(t *testing.T) {
+	path := filepath.Join(os.TempDir(), fmt.Sprintf("goleveldb-testrolock-%d", os.Getuid()))
+	if err := os.RemoveAll(path); err != nil && !os.IsNotExist(err) {
+		t.Fatal("RemoveAll: got error: ", err)
+	}
+	defer os.RemoveAll(path)
+
+	p1, err := OpenFile(path, false)
+	if err != nil {
+		t.Fatal("OpenFile(1): got error: ", err)
+	}
+
+	_, err = OpenFile(path, true)
+	if err != nil {
+		t.Logf("OpenFile(2): got error: %s (expected)", err)
+	} else {
+		t.Fatal("OpenFile(2): expect error")
+	}
+
+	p1.Close()
+
+	p3, err := OpenFile(path, true)
+	if err != nil {
+		t.Fatal("OpenFile(3): got error: ", err)
+	}
+
+	p4, err := OpenFile(path, true)
+	if err != nil {
+		t.Fatal("OpenFile(4): got error: ", err)
+	}
+
+	_, err = OpenFile(path, false)
+	if err != nil {
+		t.Logf("OpenFile(5): got error: %s (expected)", err)
+	} else {
+		t.Fatal("OpenFile(2): expect error")
+	}
+
+	p3.Close()
+	p4.Close()
+}
diff --git a/leveldb/storage/file_storage_unix.go b/leveldb/storage/file_storage_unix.go
index 6eb3274..7e29915 100644
--- a/leveldb/storage/file_storage_unix.go
+++ b/leveldb/storage/file_storage_unix.go
@@ -18,18 +18,27 @@
 }
 
 func (fl *unixFileLock) release() error {
-	if err := setFileLock(fl.f, false); err != nil {
+	if err := setFileLock(fl.f, false, false); err != nil {
 		return err
 	}
 	return fl.f.Close()
 }
 
-func newFileLock(path string) (fl fileLock, err error) {
-	f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)
+func newFileLock(path string, readOnly bool) (fl fileLock, err error) {
+	var flag int
+	if readOnly {
+		flag = os.O_RDONLY
+	} else {
+		flag = os.O_RDWR
+	}
+	f, err := os.OpenFile(path, flag, 0)
+	if os.IsNotExist(err) {
+		f, err = os.OpenFile(path, flag|os.O_CREATE, 0644)
+	}
 	if err != nil {
 		return
 	}
-	err = setFileLock(f, true)
+	err = setFileLock(f, readOnly, true)
 	if err != nil {
 		f.Close()
 		return
@@ -38,10 +47,14 @@
 	return
 }
 
-func setFileLock(f *os.File, lock bool) error {
+func setFileLock(f *os.File, readOnly, lock bool) error {
 	how := syscall.LOCK_UN
 	if lock {
-		how = syscall.LOCK_EX
+		if readOnly {
+			how = syscall.LOCK_SH
+		} else {
+			how = syscall.LOCK_EX
+		}
 	}
 	return syscall.Flock(int(f.Fd()), how|syscall.LOCK_NB)
 }
diff --git a/leveldb/storage/file_storage_windows.go b/leveldb/storage/file_storage_windows.go
index 50c3c45..899335f 100644
--- a/leveldb/storage/file_storage_windows.go
+++ b/leveldb/storage/file_storage_windows.go
@@ -29,12 +29,22 @@
 	return syscall.Close(fl.fd)
 }
 
-func newFileLock(path string) (fl fileLock, err error) {
+func newFileLock(path string, readOnly bool) (fl fileLock, err error) {
 	pathp, err := syscall.UTF16PtrFromString(path)
 	if err != nil {
 		return
 	}
-	fd, err := syscall.CreateFile(pathp, syscall.GENERIC_READ|syscall.GENERIC_WRITE, 0, nil, syscall.CREATE_ALWAYS, syscall.FILE_ATTRIBUTE_NORMAL, 0)
+	var access, shareMode uint32
+	if readOnly {
+		access = syscall.GENERIC_READ
+		shareMode = syscall.FILE_SHARE_READ
+	} else {
+		access = syscall.GENERIC_READ | syscall.GENERIC_WRITE
+	}
+	fd, err := syscall.CreateFile(pathp, access, shareMode, nil, syscall.OPEN_EXISTING, syscall.FILE_ATTRIBUTE_NORMAL, 0)
+	if err == syscall.ERROR_FILE_NOT_FOUND {
+		fd, err = syscall.CreateFile(pathp, access, shareMode, nil, syscall.OPEN_ALWAYS, syscall.FILE_ATTRIBUTE_NORMAL, 0)
+	}
 	if err != nil {
 		return
 	}
@@ -47,9 +57,8 @@
 	if r1 == 0 {
 		if e1 != 0 {
 			return error(e1)
-		} else {
-			return syscall.EINVAL
 		}
+		return syscall.EINVAL
 	}
 	return nil
 }
diff --git a/leveldb/testutil/storage.go b/leveldb/testutil/storage.go
index f3e4456..1d9163e 100644
--- a/leveldb/testutil/storage.go
+++ b/leveldb/testutil/storage.go
@@ -669,7 +669,7 @@
 			storageMu.Unlock()
 			path = filepath.Join(os.TempDir(), fmt.Sprintf("goleveldb-test%d0%d0%d", os.Getuid(), os.Getpid(), num))
 			if _, err := os.Stat(path); os.IsNotExist(err) {
-				stor, err = storage.OpenFile(path)
+				stor, err = storage.OpenFile(path, false)
 				ExpectWithOffset(1, err).NotTo(HaveOccurred(), "creating storage at %s", path)
 				break
 			}
diff --git a/manualtest/dbstress/main.go b/manualtest/dbstress/main.go
index 7fe639d..cf8466e 100644
--- a/manualtest/dbstress/main.go
+++ b/manualtest/dbstress/main.go
@@ -318,7 +318,7 @@
 	runtime.GOMAXPROCS(runtime.NumCPU())
 
 	os.RemoveAll(dbPath)
-	stor, err := storage.OpenFile(dbPath)
+	stor, err := storage.OpenFile(dbPath, false)
 	if err != nil {
 		log.Fatal(err)
 	}
diff --git a/manualtest/filelock/main.go b/manualtest/filelock/main.go
index 3bed1f3..192951f 100644
--- a/manualtest/filelock/main.go
+++ b/manualtest/filelock/main.go
@@ -49,7 +49,7 @@
 		fmt.Println("Child flag set.")
 	}
 
-	stor, err := storage.OpenFile(filename)
+	stor, err := storage.OpenFile(filename, false)
 	if err != nil {
 		fmt.Printf("Could not open storage: %s", err)
 		os.Exit(10)