# GNU MediaGoblin -- federated, autonomous media hosting # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from mediagoblin.storage import StorageInterface, clean_listy_filepath class MountError(Exception): pass class MountStorage(StorageInterface): """ Experimental "Mount" virtual Storage Interface This isn't an interface to some real storage, instead it's a redirecting interface, that redirects requests to other "StorageInterface"s. For example, say you have the paths: 1. ['user_data', 'cwebber', 'avatar.jpg'] 2. ['user_data', 'elrond', 'avatar.jpg'] 3. ['media_entries', '34352f304c3f4d0ad8ad0f043522b6f2', 'thumb.jpg'] You could mount media_entries under CloudFileStorage and user_data under BasicFileStorage. Then 1 would be passed to BasicFileStorage under the path ['cwebber', 'avatar.jpg'] and 3 would be passed to CloudFileStorage under ['34352f304c3f4d0ad8ad0f043522b6f2', 'thumb.jpg']. In other words, this is kind of like mounting /home/ and /etc/ under different filesystems on your operating system... but with mediagoblin filestorages :) To set this up, you currently need to call the mount() method with the target path and a backend, that shall be available under that target path. You have to mount things in a sensible order, especially you can't mount ["a", "b"] before ["a"]. """ def __init__(self, **kwargs): self.mounttab = {} def mount(self, dirpath, backend): """ Mount a new backend under dirpath """ new_ent = clean_listy_filepath(dirpath) print "Mounting:", repr(new_ent) already, rem_1, table, rem_2 = self._resolve_to_backend(new_ent, True) print "===", repr(already), repr(rem_1), repr(rem_2), len(table) assert (len(rem_2) > 0) or (None not in table), \ "That path is already mounted" assert (len(rem_2) > 0) or (len(table) == 0), \ "A longer path is already mounted here" for part in rem_2: table[part] = {} table = table[part] table[None] = backend def _resolve_to_backend(self, filepath, extra_info=False): """ extra_info = True is for internal use! Normally, returns the backend and the filepath inside that backend. With extra_info = True it returns the last directory node and the remaining filepath from there in addition. """ table = self.mounttab filepath = filepath[:] res_fp = None while True: new_be = table.get(None) if (new_be is not None) or res_fp is None: res_be = new_be res_fp = filepath[:] res_extra = (table, filepath[:]) # print "... New res: %r, %r, %r" % (res_be, res_fp, res_extra) if len(filepath) == 0: break query = filepath.pop(0) entry = table.get(query) if entry is not None: table = entry res_extra = (table, filepath[:]) else: break if extra_info: return (res_be, res_fp) + res_extra else: return (res_be, res_fp) def resolve_to_backend(self, filepath): backend, filepath = self._resolve_to_backend(filepath) if backend is None: raise MountError("Path not mounted") return backend, filepath def __repr__(self, table=None, indent=[]): res = [] if table is None: res.append("MountStorage<") table = self.mounttab v = table.get(None) if v: res.append(" " * len(indent) + repr(indent) + ": " + repr(v)) for k, v in table.iteritems(): if k == None: continue res.append(" " * len(indent) + repr(k) + ":") res += self.__repr__(v, indent + [k]) if table is self.mounttab: res.append(">") return "\n".join(res) else: return res def file_exists(self, filepath): backend, filepath = self.resolve_to_backend(filepath) return backend.file_exists(filepath) def get_file(self, filepath, mode='r'): backend, filepath = self.resolve_to_backend(filepath) return backend.get_file(filepath, mode) def delete_file(self, filepath): backend, filepath = self.resolve_to_backend(filepath) return backend.delete_file(filepath) def file_url(self, filepath): backend, filepath = self.resolve_to_backend(filepath) return backend.file_url(filepath) def get_local_path(self, filepath): backend, filepath = self.resolve_to_backend(filepath) return backend.get_local_path(filepath) def copy_locally(self, filepath, dest_path): """ Need to override copy_locally, because the local_storage attribute is not correct. """ backend, filepath = self.resolve_to_backend(filepath) backend.copy_locally(filepath, dest_path)