commit 56f3c759096e3fe80b58549b66560a7a0c65d811
Author: Sofia Papagiannaki <papagian@gmail.com>
Date:   Wed Mar 27 18:49:07 2013 +0200

    Change pithos public implementation
    
    Public URLs should consist of a random part.
    A public URL should be unique and should not be
    equal to any deleted public URL.
    Finally publishing a public object has no effect.
    Therefore, to generate a new public URL for an
    object, the object should be unpublished and
    published again.

diff --git a/contrib/migrate-db b/contrib/migrate-db
index 2286bbb..c86b010 100755
--- a/contrib/migrate-db
+++ b/contrib/migrate-db
@@ -166,7 +166,11 @@ class ObjectMigration(Migration):
             self.create_tags(headerid, nodeid, vserials)
             #set object's publicity
             if public:
-                self.backend.permissions.public_set(object)
+                self.backend.permissions.public_set(
+                    object,
+                    self.backend.public_url_min_length,
+                    self.backend.public_url_alphabet
+                )
             #set object's permissions
             self.create_permissions(headerid, object, username, is_folder=False)
     
diff --git a/snf-pithos-app/README b/snf-pithos-app/README
index b23e97e..d4d51db 100644
--- a/snf-pithos-app/README
+++ b/snf-pithos-app/README
@@ -27,37 +27,39 @@ Settings
 
 Configure in ``settings.py`` or a ``.conf`` file in ``/etc/synnefo`` if using snf-webproject.
 
-===============================  =================================================  ============================================================
-Name                             Default value                                      Description
-===============================  =================================================  ============================================================
-PITHOS_AUTHENTICATION_URL        \https://<astakos.host>/im/authenticate/           Astakos Authentication URL
-PITHOS_AUTHENTICATION_USERS      A dictionary of sample users (token to username)   Set to empty or None to disable
-PITHOS_ASTAKOS_COOKIE_NAME       _pithos2_a                                         Cookie name to retrieve fallback token
+===============================  ================================================================   ============================================================
+Name                             Default value                                                      Description
+===============================  ================================================================   ============================================================
+PITHOS_AUTHENTICATION_URL        \https://<astakos.host>/im/authenticate/                           Astakos Authentication URL
+PITHOS_AUTHENTICATION_USERS      A dictionary of sample users (token to username)                   Set to empty or None to disable
+PITHOS_ASTAKOS_COOKIE_NAME       _pithos2_a                                                         Cookie name to retrieve fallback token
 PITHOS_BACKEND_DB_MODULE         pithos.backends.lib.sqlalchemy
-PITHOS_BACKEND_DB_CONNECTION     sqlite:////tmp/pithos-backend.db                   SQLAlchemy database connection string
-PITHOS_BACKEND_BLOCK_MODULE      pithos.backends.lib.hashfiler
-PITHOS_BACKEND_BLOCK_PATH        /tmp/pithos-data/                                  Map and block storage path
-PITHOS_BACKEND_BLOCK_UMASK       0o022                                              Map and block storage umask
-PITHOS_BACKEND_QUEUE_MODULE      None                                               Use ``pithos.backends.lib.rabbitmq`` to enable
-PITHOS_BACKEND_QUEUE_HOSTS       None                                               Format like [``amqp://guest:guest@localhost:5672``
+PITHOS_BACKEND_DB_CONNECTION     sqlite:////tmp/pithos-backend.db                                   SQLAlchemy database connection string
+PITHOS_BACKEND_BLOCK_MODULE      pithos.backends.lib.hashfile
+PITHOS_BACKEND_BLOCK_PATH        /tmp/pithos-data/                                                  Map and block storage path
+PITHOS_BACKEND_BLOCK_UMASK       0o022                                                              Map and block storage umask
+PITHOS_BACKEND_QUEUE_MODULE      None                                                               Use ``pithos.backends.lib.rabbitmq`` to enable
+PITHOS_BACKEND_QUEUE_HOSTS       None                                                               Format like [``amqp://guest:guest@localhost:5672``
 PITHOS_BACKEND_QUEUE_EXCHANGE    pithos
-PITHOS_BACKEND_QUOTA             50 GB (50 * 1024 ** 3)                             Default user quota
-PITHOS_BACKEND_VERSIONING        auto                                               Default versioning policy for containers
-PITHOS_BACKEND_FREE_VERSIONING   True                                               Default versioning debit policy (default free)
-PITHOS_UPDATE_MD5                True                                               Update object checksums when using hashmaps
-PITHOS_SERVICE_TOKEN             ''                                                 Service token acquired by the identity provider (astakos)
-PITHOS_RADOS_STORAGE             False                                              Enables or disables secondary Pithos storage on RADOS
-PITHOS_RADOS_POOL_BLOCKS         None                                               RADOS pool to be used for block storage
-PITHOS_RADOS_POOL_MAPS           None                                               RADOS pool to be used for maps storage
-PITHOS_TRANSLATE_UUIDS           False                                              Enables a ui compatibility layer for the introduction of UUIDs in identity management.
-PITHOS_PROXY_USER_SERVICES       True                                               Whether to proxy user feedback and catalog services
-PITHOS_USER_CATALOG_URL          \https://<astakos.host>/user_catalogs/             Astakos User Catalog URL
-PITHOS_USER_FEEDBACK_URL         \https://<astakos.host>/feedback/                  Astakos User Feedback URL
-PITHOS_USER_LOGIN_URL            \https://<astakos.host>/login/                     Astakos User Login URL
-PITHOS_USE_QUOTAHOLDER           True                                               Enable quotaholder
-PITHOS_QUOTAHOLDER_URL           ''                                                 Quotaholder URL
-PITHOS_QUOTAHOLDER_TOKEN         ''                                                 Quotaholder token
-===============================  =================================================  ============================================================
+PITHOS_BACKEND_QUOTA             50 GB (50 * 1024 ** 3)                                             Default user quota
+PITHOS_BACKEND_VERSIONING        auto                                                               Default versioning policy for containers
+PITHOS_BACKEND_FREE_VERSIONING   True                                                               Default versioning debit policy (default free)
+PITHOS_UPDATE_MD5                True                                                               Update object checksums when using hashmaps
+PITHOS_SERVICE_TOKEN             ''                                                                 Service token acquired by the identity provider (astakos)
+PITHOS_RADOS_STORAGE             False                                                              Enables or disables secondary Pithos storage on RADOS
+PITHOS_RADOS_POOL_BLOCKS         None                                                               RADOS pool to be used for block storage
+PITHOS_RADOS_POOL_MAPS           None                                                               RADOS pool to be used for maps storage
+PITHOS_TRANSLATE_UUIDS           False                                                              Enables a ui compatibility layer for the introduction of UUIDs in identity management.
+PITHOS_PROXY_USER_SERVICES       True                                                               Whether to proxy user feedback and catalog services
+PITHOS_USER_CATALOG_URL          \https://<astakos.host>/user_catalogs/                             Astakos User Catalog URL
+PITHOS_USER_FEEDBACK_URL         \https://<astakos.host>/feedback/                                  Astakos User Feedback URL
+PITHOS_USER_LOGIN_URL            \https://<astakos.host>/login/                                     Astakos User Login URL
+PITHOS_USE_QUOTAHOLDER           True                                                               Enable quotaholder
+PITHOS_QUOTAHOLDER_URL           ''                                                                 Quotaholder URL
+PITHOS_QUOTAHOLDER_TOKEN         ''                                                                 Quotaholder token
+PITHOS_PUBLIC_URL_MIN_LENGTH     8                                                                  Public URL minimun length
+PITHOS_PUBLIC_URL_ALPHABET       '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'   Public URL alphabet
+===============================  ================================================================   ============================================================
 
 To update checksums asynchronously, enable the queue, install snf-pithos-tools and use ``pithos-dispatcher``::
 
diff --git a/snf-pithos-app/conf/20-snf-pithos-app-settings.conf b/snf-pithos-app/conf/20-snf-pithos-app-settings.conf
index 477f1e6..bf4d827 100644
--- a/snf-pithos-app/conf/20-snf-pithos-app-settings.conf
+++ b/snf-pithos-app/conf/20-snf-pithos-app-settings.conf
@@ -51,7 +51,10 @@
 #
 # Tune the size of the http pool for the quotaholder client.
 # It limits the maximum number of quota changing requests
-# that pithos can serve. Extra requests will be blocked 
+# that pithos can serve. Extra requests will be blocked
 # until another has completed.
 #
 #PITHOS_QUOTAHOLDER_POOLSIZE = 200
+#
+# Set public url length
+#PITHOS_PUBLIC_URL_MIN_LENGTH = 8
diff --git a/snf-pithos-app/pithos/api/public.py b/snf-pithos-app/pithos/api/public.py
index 3b16b3f..53ed3e2 100644
--- a/snf-pithos-app/pithos/api/public.py
+++ b/snf-pithos-app/pithos/api/public.py
@@ -44,7 +44,6 @@ from pithos.api.util import (put_object_headers, update_manifest_meta,
                              validate_matching_preconditions,
                              object_data_response, api_method,
                              split_container_object_string)
-from pithos.api.short_url import decode_url
 from pithos.api.settings import AUTHENTICATION_URL, AUTHENTICATION_USERS
 
 
@@ -72,7 +71,7 @@ def public_meta(request, v_public):
     try:
         v_account, v_container, v_object = request.backend.get_public(
             request.user_uniq,
-            decode_url(v_public))
+            v_public)
         meta = request.backend.get_object_meta(request.user_uniq, v_account,
                                                v_container, v_object, 'pithos')
         public = request.backend.get_object_public(
@@ -99,10 +98,11 @@ def public_read(request, v_public):
     #                       itemNotFound (404),
     #                       badRequest (400),
     #                       notModified (304)
+
     try:
         v_account, v_container, v_object = request.backend.get_public(
             request.user_uniq,
-            decode_url(v_public))
+            v_public)
         meta = request.backend.get_object_meta(request.user_uniq, v_account,
                                                v_container, v_object, 'pithos')
         public = request.backend.get_object_public(
diff --git a/snf-pithos-app/pithos/api/settings.py b/snf-pithos-app/pithos/api/settings.py
index c4e7dc5..f1b7a4b 100644
--- a/snf-pithos-app/pithos/api/settings.py
+++ b/snf-pithos-app/pithos/api/settings.py
@@ -65,3 +65,11 @@ USE_QUOTAHOLDER = getattr(settings, 'PITHOS_USE_QUOTAHOLDER', False)
 QUOTAHOLDER_URL = getattr(settings, 'PITHOS_QUOTAHOLDER_URL', '')
 QUOTAHOLDER_TOKEN = getattr(settings, 'PITHOS_QUOTAHOLDER_TOKEN', '')
 QUOTAHOLDER_POOLSIZE = getattr(settings, 'PITHOS_QUOTAHOLDER_POOLSIZE', 200)
+
+# Set public url length and alphabet
+PUBLIC_URL_MIN_LENGTH =  getattr(settings, 'PITHOS_PUBLIC_URL_MIN_LENGTH', 8)
+PUBLIC_URL_ALPHABET =  getattr(
+    settings,
+    'PITHOS_PUBLIC_URL_ALPHABET',
+    '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
+)
diff --git a/snf-pithos-app/pithos/api/swiss_army/__init__.py b/snf-pithos-app/pithos/api/swiss_army/__init__.py
index eccb628..ca76a5a 100644
--- a/snf-pithos-app/pithos/api/swiss_army/__init__.py
+++ b/snf-pithos-app/pithos/api/swiss_army/__init__.py
@@ -194,7 +194,11 @@ class SwissArmy():
         if public:
             # set destination object public
             fullpath = '/'.join([dest_account, dest_container, dest_name])
-            self.backend.permissions.public_set(fullpath)
+            self.backend.permissions.public_set(
+                fullpath,
+                self.backend.public_url_min_length,
+                self.backend.public_url_alphabet
+            )
 
     def _merge_account(self, src_account, dest_account, delete_src=False):
             # TODO: handle exceptions
diff --git a/snf-pithos-app/pithos/api/tests.py b/snf-pithos-app/pithos/api/tests.py
new file mode 100644
index 0000000..1f713f1
--- /dev/null
+++ b/snf-pithos-app/pithos/api/tests.py
@@ -0,0 +1,282 @@
+# Copyright 2011 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+#
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+
+import unittest
+import random
+import string
+import datetime
+import time as _time
+
+import pithos.api.settings as settings
+
+from pithos.api.swiss_army import SwissArmy
+
+def get_random_data(length=500):
+    char_set = string.ascii_uppercase + string.digits
+    return ''.join(random.choice(char_set) for x in xrange(length))
+
+class TestPublic(unittest.TestCase):
+    def setUp(self):
+        self.utils = SwissArmy()
+        self.backend = self.utils.backend
+        self.utils.create_account('account')
+
+    def tearDown(self):
+        self.utils._delete_account('account')
+        self.utils.cleanup()
+
+    def assert_not_public_object(self, account, container, object):
+        public = self.backend.get_object_public(
+            account, account, container, object
+        )
+        self.assertTrue(public == None)
+        self.assertRaises(
+            NameError,
+            self.backend.get_public,
+            '$$account$$',
+            public
+        )
+        self.assertRaises(
+            Exception, self.backend._can_read,
+            '$$account$$', account, container, object
+        )
+        return public
+
+    def assert_public_object(self, account, container, object):
+        public = self.backend.get_object_public(
+            account, account, container, object
+        )
+        self.assertTrue(public != None)
+        self.assertTrue(len(public) >= settings.PUBLIC_URL_MIN_LENGTH)
+        self.assertTrue(set(public) <= set(settings.PUBLIC_URL_ALPHABET))
+        self.assertEqual(
+            self.backend.get_public('$$account$$', public),
+            (account, container, object)
+        )
+        try:
+            self.backend._can_read('$$account$$', account, container, object)
+        except Exception:
+            self.fail('Public object should be readable.')
+        return public
+
+    def test_set_object_public(self):
+        self.utils.backend.put_container('account', 'account', 'container')
+        data = get_random_data(int(random.random()))
+        self.utils.create_update_object(
+            'account',
+            'container',
+            'object',
+            'application/octet-stream',
+            data
+        )
+        self.assert_not_public_object('account', 'container', 'object')
+
+        self.backend.permissions.public_set(
+            'account/container/object',
+            self.backend.public_url_min_length,
+            self.backend.public_url_alphabet
+        )
+        self.assert_public_object('account', 'container', 'object')
+
+    def test_set_twice(self):
+        self.utils.backend.put_container('account', 'account', 'container')
+        data = get_random_data(int(random.random()))
+        self.utils.create_update_object(
+            'account',
+            'container',
+            'object',
+            'application/octet-stream',
+            data
+        )
+        self.backend.permissions.public_set(
+            'account/container/object',
+            self.backend.public_url_min_length,
+            self.backend.public_url_alphabet
+        )
+        public = self.assert_public_object('account', 'container', 'object')
+
+        self.backend.permissions.public_set(
+            'account/container/object',
+            self.backend.public_url_min_length,
+            self.backend.public_url_alphabet
+        )
+        public2 = self.assert_public_object('account', 'container', 'object')
+
+        self.assertEqual(public, public2)
+
+    def test_set_unset_set(self):
+        self.utils.backend.put_container('account', 'account', 'container')
+        data = get_random_data(int(random.random()))
+        self.utils.create_update_object(
+            'account',
+            'container',
+            'object',
+            'application/octet-stream',
+            data
+        )
+        self.backend.permissions.public_set(
+            'account/container/object',
+            self.backend.public_url_min_length,
+            self.backend.public_url_alphabet
+        )
+        public = self.assert_public_object('account', 'container', 'object')
+
+        self.backend.permissions.public_unset('account/container/object')
+        self.assert_not_public_object('account', 'container', 'object')
+
+        self.backend.permissions.public_set(
+            'account/container/object',
+            self.backend.public_url_min_length,
+            self.backend.public_url_alphabet
+        )
+        public3 = self.assert_public_object('account', 'container', 'object')
+
+        self.assertTrue(public != public3)
+
+    def test_update_object_public(self):
+        self.utils.backend.put_container('account', 'account', 'container')
+        data = get_random_data(int(random.random()))
+        self.utils.create_update_object(
+            'account',
+            'container',
+            'object',
+            'application/octet-stream',
+            data
+        )
+
+        self.backend.update_object_public(
+            'account', 'account', 'container', 'object', public=False
+        )
+        self.assert_not_public_object('account', 'container', 'object')
+
+        self.backend.update_object_public(
+            'account', 'account', 'container', 'object', public=True
+        )
+        public = self.assert_public_object('account', 'container', 'object')
+
+        self.backend.update_object_public(
+            'account', 'account', 'container', 'object', public=False
+        )
+        self.assert_not_public_object('account', 'container', 'object')
+
+        self.backend.update_object_public(
+            'account', 'account', 'container', 'object', public=True
+        )
+        new_public = self.assert_public_object('account', 'container', 'object')
+        self.assertTrue(public != new_public)
+
+    def test_delete_not_public_object(self):
+        self.utils.backend.put_container('account', 'account', 'container')
+        data = get_random_data(int(random.random()))
+        self.utils.create_update_object(
+            'account',
+            'container',
+            'object',
+            'application/octet-stream',
+            data
+        )
+        self.assert_not_public_object('account', 'container', 'object')
+
+        self.backend.delete_object('account', 'account', 'container', 'object')
+
+        self.assert_not_public_object('account', 'container', 'object')
+
+    def test_delete_public_object(self):
+        self.utils.backend.put_container('account', 'account', 'container')
+        data = get_random_data(int(random.random()))
+        self.utils.create_update_object(
+            'account',
+            'container',
+            'object',
+            'application/octet-stream',
+            data
+        )
+        self.assert_not_public_object('account', 'container', 'object')
+
+        self.backend.permissions.public_set(
+            'account/container/object',
+            self.backend.public_url_min_length,
+            self.backend.public_url_alphabet
+        )
+        self.assert_public_object('account', 'container', 'object')
+
+        self.backend.delete_object('account', 'account', 'container', 'object')
+        self.assert_not_public_object('account', 'container', 'object')
+
+    def test_delete_public_object_history(self):
+        self.utils.backend.put_container('account', 'account', 'container')
+        for i in range(random.randint(1, 10)):
+            data = get_random_data(int(random.random()))
+            self.utils.create_update_object(
+                'account',
+                'container',
+                'object',
+                'application/octet-stream',
+                data
+            )
+            _time.sleep(1)
+        versions = self.backend.list_versions(
+            'account', 'account', 'container', 'object'
+        )
+        mtime = [int(i[1]) for i in versions]
+        self.assert_not_public_object('account', 'container', 'object')
+
+        self.backend.permissions.public_set(
+            'account/container/object',
+            self.backend.public_url_min_length,
+            self.backend.public_url_alphabet
+        )
+        public = self.assert_public_object('account', 'container', 'object')
+
+        i = random.randrange(len(mtime))
+        self.backend.delete_object(
+            'account', 'account', 'container', 'object', until=mtime[i]
+        )
+        self.assert_public_object('account', 'container', 'object')
+        public = self.assert_public_object('account', 'container', 'object')
+
+        _time.sleep(1)
+        t = datetime.datetime.utcnow()
+        now = int(_time.mktime(t.timetuple()))
+        self.backend.delete_object(
+            'account', 'account', 'container', 'object', until=now
+        )
+        self.assertRaises(
+            NameError,
+            self.backend.get_public,
+            '$$account$$',
+            public
+        )
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/snf-pithos-app/pithos/api/util.py b/snf-pithos-app/pithos/api/util.py
index 79bf557..2a2bbfa 100644
--- a/snf-pithos-app/pithos/api/util.py
+++ b/snf-pithos-app/pithos/api/util.py
@@ -55,7 +55,6 @@ from pithos.api.faults import (
     Fault, NotModified, BadRequest, Unauthorized, Forbidden, ItemNotFound,
     Conflict, LengthRequired, PreconditionFailed, RequestEntityTooLarge,
     RangeNotSatisfiable, InternalServerError, NotImplemented)
-from pithos.api.short_url import encode_url
 from pithos.api.settings import (BACKEND_DB_MODULE, BACKEND_DB_CONNECTION,
                                  BACKEND_BLOCK_MODULE, BACKEND_BLOCK_PATH,
                                  BACKEND_BLOCK_UMASK,
@@ -68,7 +67,9 @@ from pithos.api.settings import (BACKEND_DB_MODULE, BACKEND_DB_CONNECTION,
                                  AUTHENTICATION_URL, AUTHENTICATION_USERS,
                                  COOKIE_NAME, USER_CATALOG_URL,
                                  RADOS_STORAGE, RADOS_POOL_BLOCKS,
-                                 RADOS_POOL_MAPS, TRANSLATE_UUIDS)
+                                 RADOS_POOL_MAPS, TRANSLATE_UUIDS,
+                                 PUBLIC_URL_MIN_LENGTH,
+                                 PUBLIC_URL_ALPHABET)
 from pithos.backends import connect_backend
 from pithos.backends.base import (NotAllowedError, QuotaError, ItemNotExists,
                                   VersionNotExists)
@@ -387,7 +388,7 @@ def update_sharing_meta(request, permissions, v_account, v_container, v_object,
 def update_public_meta(public, meta):
     if not public:
         return
-    meta['X-Object-Public'] = '/public/' + encode_url(public)
+    meta['X-Object-Public'] = '/public/' + public
 
 
 def validate_modification_preconditions(request, meta):
@@ -983,7 +984,9 @@ _pithos_backend_pool = PithosBackendPool(
         quotaholder_token=QUOTAHOLDER_TOKEN,
         quotaholder_client_poolsize=QUOTAHOLDER_POOLSIZE,
         free_versioning=BACKEND_FREE_VERSIONING,
-        block_params=BLOCK_PARAMS)
+        block_params=BLOCK_PARAMS,
+        public_url_min_length=PUBLIC_URL_MIN_LENGTH,
+        public_url_alphabet=PUBLIC_URL_ALPHABET)
 
 def get_backend():
     backend = _pithos_backend_pool.pool_get()
diff --git a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/27381099d477_alter_public_add_col.py b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/27381099d477_alter_public_add_col.py
new file mode 100644
index 0000000..77f34b9
--- /dev/null
+++ b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/alembic/versions/27381099d477_alter_public_add_col.py
@@ -0,0 +1,39 @@
+"""alter public add column url
+
+Revision ID: 27381099d477
+Revises: 2a309a9a3438
+Create Date: 2013-03-20 16:14:20.058077
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '27381099d477'
+down_revision = '2a309a9a3438'
+
+from alembic import op
+import sqlalchemy as sa
+
+from pithos.backends.modular import ULTIMATE_ANSWER
+from pithos.api.short_url import encode_url
+
+def upgrade():
+    op.add_column('public', sa.Column('url', sa.String(2048)))
+    op.create_unique_constraint('idx_public_url', 'public', ['url'])
+
+    # migrate old rows
+    p = sa.sql.table(
+        'public',
+        sa.sql.column('public_id', sa.Integer),
+        sa.sql.column('url', sa.String),
+    )
+    get_url = lambda x: encode_url(x + ULTIMATE_ANSWER)
+    conn = op.get_bind()
+    s = sa.select([p.c.public_id])
+    rows = conn.execute(s).fetchall()
+    for r in rows:
+        s = p.update().values(url=get_url(r[0])).where(p.c.public_id==r[0])
+        op.execute(s)
+
+def downgrade():
+    op.drop_constraint('idx_public_url', 'public')
+    op.drop_column('public', 'url')
diff --git a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/public.py b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/public.py
index 663c479..5143a49 100644
--- a/snf-pithos-backend/pithos/backends/lib/sqlalchemy/public.py
+++ b/snf-pithos-backend/pithos/backends/lib/sqlalchemy/public.py
@@ -37,6 +37,7 @@ from sqlalchemy.sql import and_, select
 from sqlalchemy.schema import Index
 from sqlalchemy.exc import NoSuchTableError
 
+from pithos.backends.random_word import get_word
 
 def create_tables(engine):
     metadata = MetaData()
@@ -44,10 +45,13 @@ def create_tables(engine):
     columns.append(Column('public_id', Integer, primary_key=True))
     columns.append(Column('path', String(2048), nullable=False))
     columns.append(Column('active', Boolean, nullable=False, default=True))
+    columns.append(Column('url', String(2048), nullable=True))
     public = Table('public', metadata, *columns, mysql_engine='InnoDB',
                    sqlite_autoincrement=True)
     # place an index on path
     Index('idx_public_path', public.c.path, unique=True)
+    # place an index on url
+    Index('idx_public_url', public.c.url, unique=True)
     metadata.create_all(engine)
     return metadata.sorted_tables
 
@@ -64,39 +68,49 @@ class Public(DBWorker):
             tables = create_tables(self.engine)
             map(lambda t: self.__setattr__(t.name, t), tables)
 
-    def public_set(self, path):
+    def get_unique_url(self, serial, public_url_min_length, public_url_alphabet):
+        l = public_url_min_length
+        while 1:
+            candidate = get_word(serial, length=l, alphabet=public_url_alphabet)
+            if self.public_path(candidate) is None:
+                return candidate
+            l +=1
+
+    def public_set(self, path, public_url_min_length, public_url_alphabet):
         s = select([self.public.c.public_id])
         s = s.where(self.public.c.path == path)
         r = self.conn.execute(s)
         row = r.fetchone()
         r.close()
-        if row:
-            s = self.public.update().where(self.public.c.public_id == row[0])
-            s = s.values(active=True)
-        else:
+
+        if not row:
             s = self.public.insert()
             s = s.values(path=path, active=True)
-        r = self.conn.execute(s)
-        r.close()
+            r = self.conn.execute(s)
+            serial = r.inserted_primary_key[0]
+            r.close()
+
+            url = self.get_unique_url(
+                serial, public_url_min_length, public_url_alphabet
+            )
+            s = self.public.update().where(self.public.c.public_id == serial)
+            s = s.values(url=url)
+            self.conn.execute(s).close()
 
     def public_unset(self, path):
-        s = self.public.update()
+        s = self.public.delete()
         s = s.where(self.public.c.path == path)
-        s = s.values(active=False)
-        r = self.conn.execute(s)
-        r.close()
+        self.conn.execute(s).close()
 
     def public_unset_bulk(self, paths):
         if not paths:
             return
-        s = self.public.update()
+        s = self.public.delete()
         s = s.where(self.public.c.path.in_(paths))
-        s = s.values(active=False)
-        r = self.conn.execute(s)
-        r.close()
+        self.conn.execute(s).close()
 
     def public_get(self, path):
-        s = select([self.public.c.public_id])
+        s = select([self.public.c.url])
         s = s.where(and_(self.public.c.path == path,
                          self.public.c.active == True))
         r = self.conn.execute(s)
@@ -107,7 +121,7 @@ class Public(DBWorker):
         return None
 
     def public_list(self, prefix):
-        s = select([self.public.c.path, self.public.c.public_id])
+        s = select([self.public.c.path, self.public.c.url])
         s = s.where(self.public.c.path.like(
             self.escape_like(prefix) + '%', escape='\\'))
         s = s.where(self.public.c.active == True)
@@ -118,7 +132,7 @@ class Public(DBWorker):
 
     def public_path(self, public):
         s = select([self.public.c.path])
-        s = s.where(and_(self.public.c.public_id == public,
+        s = s.where(and_(self.public.c.url == public,
                          self.public.c.active == True))
         r = self.conn.execute(s)
         row = r.fetchone()
diff --git a/snf-pithos-backend/pithos/backends/lib/sqlite/public.py b/snf-pithos-backend/pithos/backends/lib/sqlite/public.py
index 6c6b8c6..020520f 100644
--- a/snf-pithos-backend/pithos/backends/lib/sqlite/public.py
+++ b/snf-pithos-backend/pithos/backends/lib/sqlite/public.py
@@ -33,6 +33,7 @@
 
 from dbworker import DBWorker
 
+from pithos.backends.random_word import get_word
 
 class Public(DBWorker):
     """Paths can be marked as public."""
@@ -44,27 +45,46 @@ class Public(DBWorker):
         execute(""" create table if not exists public
                           ( public_id integer primary key autoincrement,
                             path      text not null,
-                            active    boolean not null default 1 ) """)
+                            active    boolean not null default 1,
+                            url       text) """)
         execute(""" create unique index if not exists idx_public_path
                     on public(path) """)
+        execute(""" create unique index if not exists idx_public_url
+                    on public(url) """)
 
-    def public_set(self, path):
-        q = "insert or ignore into public (path) values (?)"
-        self.execute(q, (path,))
-        q = "update public set active = 1 where path = ?"
+    def get_unique_url(self, serial, public_url_min_length, public_url_alphabet):
+        l = public_url_min_length
+        while 1:
+            candidate = get_word(serial, length=l, alphabet=public_url_alphabet)
+            if self.public_path(candidate) is None:
+                return candidate
+            l +=1
+
+    def public_set(self, path, public_url_min_length, public_url_alphabet):
+        q = "select public_id from public where path = ?"
         self.execute(q, (path,))
+        row = self.fetchone()
+
+        if not row:
+            q = "insert into public(path, active) values(?, ?)"
+            serial = self.execute(q, (path, active)).lastrowid
+            url = self.get_unique_url(
+                serial, public_url_min_length, public_url_alphabet
+            )
+            q = "update public set url=url where public_id = ?"
+            self.execute(q, (serial,))
 
     def public_unset(self, path):
-        q = "update public set active = 0 where path = ?"
+        q = "delete from public where path = ?"
         self.execute(q, (path,))
 
     def public_unset_bulk(self, paths):
         placeholders = ','.join('?' for path in paths)
-        q = "update public set active = 0 where path in (%s)" % placeholders
+        q = "delete from public where path in (%s)"
         self.execute(q, paths)
 
     def public_get(self, path):
-        q = "select public_id from public where path = ? and active = 1"
+        q = "select url from public where path = ? and active = 1"
         self.execute(q, (path,))
         row = self.fetchone()
         if row:
@@ -72,12 +92,12 @@ class Public(DBWorker):
         return None
 
     def public_list(self, prefix):
-        q = "select path, public_id from public where path like ? escape '\\' and active = 1"
+        q = "select path, url from public where path like ? escape '\\' and active = 1"
         self.execute(q, (self.escape_like(prefix) + '%',))
         return self.fetchall()
 
     def public_path(self, public):
-        q = "select path from public where public_id = ? and active = 1"
+        q = "select path from public where url = ? and active = 1"
         self.execute(q, (public,))
         row = self.fetchone()
         if row:
diff --git a/snf-pithos-backend/pithos/backends/modular.py b/snf-pithos-backend/pithos/backends/modular.py
index bcdd439..a1d58bf 100644
--- a/snf-pithos-backend/pithos/backends/modular.py
+++ b/snf-pithos-backend/pithos/backends/modular.py
@@ -84,6 +84,10 @@ DEFAULT_BLOCK_UMASK = 0o022
 DEFAULT_BLOCK_PARAMS = { 'mappool': None, 'blockpool': None }
 #DEFAULT_QUEUE_HOSTS = '[amqp://guest:guest@localhost:5672]'
 #DEFAULT_QUEUE_EXCHANGE = 'pithos'
+DEFAULT_ALPHABET = ('0123456789'
+                    'abcdefghijklmnopqrstuvwxyz'
+                    'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
+DEFAULT_MIN_LENGTH = 8
 
 QUEUE_MESSAGE_KEY_PREFIX = 'pithos.%s'
 QUEUE_CLIENT_ID = 'pithos'
@@ -148,7 +152,9 @@ class ModularBackend(BaseBackend):
                  quotaholder_enabled=False,
                  quotaholder_url=None, quotaholder_token=None,
                  quotaholder_client_poolsize=None,
-                 free_versioning=True, block_params=None):
+                 free_versioning=True, block_params=None,
+                 public_url_min_length=None,
+                 public_url_alphabet=None):
         db_module = db_module or DEFAULT_DB_MODULE
         db_connection = db_connection or DEFAULT_DB_CONNECTION
         block_module = block_module or DEFAULT_BLOCK_MODULE
@@ -161,6 +167,9 @@ class ModularBackend(BaseBackend):
         #queue_hosts = queue_hosts or DEFAULT_QUEUE_HOSTS
         #queue_exchange = queue_exchange or DEFAULT_QUEUE_EXCHANGE
 
+        self.public_url_min_length = public_url_min_length or DEFAULT_MIN_LENGTH
+        self.public_url_alphabet = public_url_alphabet or DEFAULT_ALPHABET
+
         self.hash_algorithm = 'sha256'
         self.block_size = 4 * 1024 * 1024  # 4MB
         self.free_versioning = free_versioning
@@ -718,7 +727,7 @@ class ModularBackend(BaseBackend):
                      account, container, prefix)
         public = {}
         for path, p in self.permissions.public_list('/'.join((account, container, prefix))):
-            public[path] = p + ULTIMATE_ANSWER
+            public[path] = p
         return public
 
     @backend_method
@@ -815,8 +824,6 @@ class ModularBackend(BaseBackend):
         self._can_read(user, account, container, name)
         path = self._lookup_object(account, container, name)[0]
         p = self.permissions.public_get(path)
-        if p is not None:
-            p += ULTIMATE_ANSWER
         return p
 
     @backend_method
@@ -830,7 +837,9 @@ class ModularBackend(BaseBackend):
         if not public:
             self.permissions.public_unset(path)
         else:
-            self.permissions.public_set(path)
+            self.permissions.public_set(
+                path, self.public_url_min_length, self.public_url_alphabet
+            )
 
     @backend_method
     def get_object_hashmap(self, user, account, container, name, version=None):
@@ -1097,9 +1106,7 @@ class ModularBackend(BaseBackend):
         """Return the (account, container, name) for the public id given."""
 
         logger.debug("get_public: %s %s", user, public)
-        if public is None or public < ULTIMATE_ANSWER:
-            raise NameError
-        path = self.permissions.public_path(public - ULTIMATE_ANSWER)
+        path = self.permissions.public_path(public)
         if path is None:
             raise NameError
         account, container, name = path.split('/', 2)
diff --git a/snf-pithos-backend/pithos/backends/random_word.py b/snf-pithos-backend/pithos/backends/random_word.py
new file mode 100644
index 0000000..86f1cba
--- /dev/null
+++ b/snf-pithos-backend/pithos/backends/random_word.py
@@ -0,0 +1,63 @@
+# Copyright 2011-2012 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+#
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+
+DEFAULT_ALPHABET = ('0123456789'
+                    'abcdefghijklmnopqrstuvwxyz'
+                    'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
+DEFAULT_MIN_LENGTH = 8
+
+from  random import randrange
+from math import log
+
+def get_random_word(length=DEFAULT_MIN_LENGTH, alphabet=DEFAULT_ALPHABET):
+    alphabet_length = len(alphabet)
+    word = ''.join(alphabet[randrange(alphabet_length)]
+        for _ in xrange(length))
+    return word
+
+def encode_serial(serial, alphabet=DEFAULT_ALPHABET):
+    i = 0
+    base = len(alphabet)
+    quotient = int(serial)
+    digits = []
+    append = digits.append
+    while quotient > 0:
+        quotient, remainder = divmod(quotient, base)
+        append(alphabet[remainder])
+    word = ''.join(reversed(digits))
+    return word
+
+def get_word(serial, length=DEFAULT_MIN_LENGTH, alphabet=DEFAULT_ALPHABET):
+    word = encode_serial(serial, alphabet)
+    word += get_random_word(length, alphabet)
+    return word
diff --git a/snf-pithos-backend/pithos/backends/util.py b/snf-pithos-backend/pithos/backends/util.py
index e2da82b..239cd36 100644
--- a/snf-pithos-backend/pithos/backends/util.py
+++ b/snf-pithos-backend/pithos/backends/util.py
@@ -48,7 +48,9 @@ class PithosBackendPool(ObjectPool):
                  quotaholder_enabled=True,
                  quotaholder_url=None, quotaholder_token=None,
                  quotaholder_client_poolsize=None,
-                 block_params=None):
+                 block_params=None,
+                 public_url_min_length=None,
+                 public_url_alphabet=None):
         super(PithosBackendPool, self).__init__(size=size)
         self.db_module = db_module
         self.db_connection = db_connection
@@ -64,6 +66,8 @@ class PithosBackendPool(ObjectPool):
         self.quotaholder_token = quotaholder_token
         self.quotaholder_client_poolsize = quotaholder_client_poolsize
         self.free_versioning = free_versioning
+        self.public_url_min_length=public_url_min_length
+        self.public_url_alphabet=public_url_alphabet
 
     def _pool_create(self):
         backend = connect_backend(
@@ -80,7 +84,9 @@ class PithosBackendPool(ObjectPool):
                 quotaholder_url=self.quotaholder_url,
                 quotaholder_token=self.quotaholder_token,
                 quotaholder_client_poolsize=self.quotaholder_client_poolsize,
-                free_versioning=self.free_versioning)
+                free_versioning=self.free_versioning,
+                public_url_min_length=self.public_url_min_length,
+                public_url_alphabet=self.public_url_alphabet)
 
         backend._real_close = backend.close
         backend.close = instancemethod(_pooled_backend_close, backend,
