From bfeb1690414995730a2feb90eaad407166327cb4 Mon Sep 17 00:00:00 2001
From: Kirill Smelkov <kirr@nexedi.com>
Date: Tue, 4 Apr 2017 13:13:09 +0300
Subject: [PATCH] Allow internal clients to specify intended access mode -
 read-only or read-write

Most of our tools need only read access for working. However e.g.
FileStorage, when opened in read-write mode, automatically creates
database file and index.

This way if database is opened in read-write mode a simple typo in path,
e.g. to `zodb dump path` would lead to:

- new database at path will be created
- the dump will print nothing (empty database)
- exit status will be 0 (ok) and no error will be reported.

For this reason it is better tools declare access level they need so for
read-only access request we can catch it with an error from storage.

This, however, requires quite recent ZODB to work:

https://github.com/zopefoundation/ZODB/pull/153

P.S.

We don't want to force users to always specify read-only in URLs or
zconf files because:

- this is error prone
- URL or zconf can be though as of file
- when a program opens a file the program, not file, declares which type
  of access it wants.

That's why access mode declaration has to be internal.
---
 setup.py              |  2 +-
 zodbtools/util.py     | 17 ++++++++++++++++-
 zodbtools/zodbcmp.py  |  4 ++--
 zodbtools/zodbdump.py |  2 +-
 4 files changed, 20 insertions(+), 5 deletions(-)

diff --git a/setup.py b/setup.py
index a01adc2..f511100 100644
--- a/setup.py
+++ b/setup.py
@@ -19,7 +19,7 @@ setup(
     keywords    = 'zodb utility tool',
 
     packages    = find_packages(),
-    install_requires = ['ZODB', 'zodburi'],
+    install_requires = ['ZODB', 'zodburi', 'six'],
 
     entry_points= {'console_scripts': ['zodb = zodbtools.zodb:main']},
 
diff --git a/zodbtools/util.py b/zodbtools/util.py
index 9230fa7..28a04d4 100644
--- a/zodbtools/util.py
+++ b/zodbtools/util.py
@@ -18,6 +18,7 @@
 
 import hashlib
 import zodburi
+from six.moves.urllib_parse import urlsplit, urlunsplit
 
 def ashex(s):
     return s.encode('hex')
@@ -76,11 +77,25 @@ def parse_tidrange(tidrange):
 
 
 # storageFromURL opens a ZODB-storage specified by url
-def storageFromURL(url):
+# read_only specifies read or read/write mode for requested access:
+# - None: use default mode specified by url
+# - True/False: explicitly request read-only / read-write mode
+def storageFromURL(url, read_only=None):
     # no schema -> file://
     if "://" not in url:
         url = "file://" + url
 
+    # read_only -> url
+    if read_only is not None:
+        scheme, netloc, path, query, fragment = urlsplit(url)
+        # XXX this won't have effect with zconfig:// but for file:// neo://
+        #     zeo:// etc ... it works
+        if scheme != "zconfig":
+            if len(query) > 0:
+                query += "&"
+            query += "read_only=%s" % read_only
+            url = urlunsplit((scheme, netloc, path, query, fragment))
+
     stor_factory, dbkw = zodburi.resolve_uri(url)
     stor = stor_factory()
 
diff --git a/zodbtools/zodbcmp.py b/zodbtools/zodbcmp.py
index b4a7728..94bb3e7 100644
--- a/zodbtools/zodbcmp.py
+++ b/zodbtools/zodbcmp.py
@@ -157,8 +157,8 @@ def main2(argv):
             print("E: invalid tidrange: %s" % e, file=sys.stderr)
             sys.exit(2)
 
-    stor1 = storageFromURL(storurl1)
-    stor2 = storageFromURL(storurl2)
+    stor1 = storageFromURL(storurl1, read_only=True)
+    stor2 = storageFromURL(storurl2, read_only=True)
 
     zcmp = storcmp(stor1, stor2, tidmin, tidmax, verbose)
     sys.exit(1 if zcmp else 0)
diff --git a/zodbtools/zodbdump.py b/zodbtools/zodbdump.py
index f1394c3..1f49787 100644
--- a/zodbtools/zodbdump.py
+++ b/zodbtools/zodbdump.py
@@ -116,6 +116,6 @@ def main(argv):
             print("E: invalid tidrange: %s" % e, file=sys.stderr)
             sys.exit(2)
 
-    stor = storageFromURL(storurl)
+    stor = storageFromURL(storurl, read_only=True)
 
     zodbdump(stor, tidmin, tidmax, hashonly)
-- 
2.30.9