docs: Update relnotes to remove "node_modules".
[mediagoblin.git] / mediagoblin / storage / mountstorage.py
CommitLineData
a2468d18 1# GNU MediaGoblin -- federated, autonomous media hosting
cf29e8a8 2# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
a2468d18
JW
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU Affero General Public License as published by
6# the Free Software Foundation, either version 3 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Affero General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
790f31e2
BP
17from __future__ import print_function
18
386c9c7c
BP
19import six
20
dd1756ee 21from mediagoblin.storage import StorageInterface, clean_listy_filepath
a2468d18
JW
22
23
a855e92a
WKG
24class MountError(Exception):
25 pass
26
27
a2468d18
JW
28class MountStorage(StorageInterface):
29 """
30 Experimental "Mount" virtual Storage Interface
31
32 This isn't an interface to some real storage, instead it's a
33 redirecting interface, that redirects requests to other
34 "StorageInterface"s.
35
36 For example, say you have the paths:
37
38 1. ['user_data', 'cwebber', 'avatar.jpg']
39 2. ['user_data', 'elrond', 'avatar.jpg']
40 3. ['media_entries', '34352f304c3f4d0ad8ad0f043522b6f2', 'thumb.jpg']
41
42 You could mount media_entries under CloudFileStorage and user_data
43 under BasicFileStorage. Then 1 would be passed to
44 BasicFileStorage under the path ['cwebber', 'avatar.jpg'] and 3
45 would be passed to CloudFileStorage under
46 ['34352f304c3f4d0ad8ad0f043522b6f2', 'thumb.jpg'].
47
48 In other words, this is kind of like mounting /home/ and /etc/
49 under different filesystems on your operating system... but with
50 mediagoblin filestorages :)
51
52 To set this up, you currently need to call the mount() method with
53 the target path and a backend, that shall be available under that
54 target path. You have to mount things in a sensible order,
55 especially you can't mount ["a", "b"] before ["a"].
56 """
57 def __init__(self, **kwargs):
58 self.mounttab = {}
59
60 def mount(self, dirpath, backend):
61 """
62 Mount a new backend under dirpath
63 """
64 new_ent = clean_listy_filepath(dirpath)
65
790f31e2 66 print("Mounting:", repr(new_ent))
a2468d18 67 already, rem_1, table, rem_2 = self._resolve_to_backend(new_ent, True)
790f31e2 68 print("===", repr(already), repr(rem_1), repr(rem_2), len(table))
a2468d18
JW
69
70 assert (len(rem_2) > 0) or (None not in table), \
71 "That path is already mounted"
72 assert (len(rem_2) > 0) or (len(table) == 0), \
73 "A longer path is already mounted here"
74
75 for part in rem_2:
76 table[part] = {}
77 table = table[part]
78 table[None] = backend
79
80 def _resolve_to_backend(self, filepath, extra_info=False):
81 """
82 extra_info = True is for internal use!
83
84 Normally, returns the backend and the filepath inside that backend.
85
86 With extra_info = True it returns the last directory node and the
87 remaining filepath from there in addition.
88 """
89 table = self.mounttab
90 filepath = filepath[:]
91 res_fp = None
92 while True:
93 new_be = table.get(None)
94 if (new_be is not None) or res_fp is None:
95 res_be = new_be
96 res_fp = filepath[:]
97 res_extra = (table, filepath[:])
98 # print "... New res: %r, %r, %r" % (res_be, res_fp, res_extra)
99 if len(filepath) == 0:
100 break
101 query = filepath.pop(0)
102 entry = table.get(query)
103 if entry is not None:
104 table = entry
105 res_extra = (table, filepath[:])
106 else:
107 break
108 if extra_info:
109 return (res_be, res_fp) + res_extra
110 else:
111 return (res_be, res_fp)
112
113 def resolve_to_backend(self, filepath):
114 backend, filepath = self._resolve_to_backend(filepath)
115 if backend is None:
a855e92a 116 raise MountError("Path not mounted")
a2468d18
JW
117 return backend, filepath
118
119 def __repr__(self, table=None, indent=[]):
120 res = []
121 if table is None:
122 res.append("MountStorage<")
123 table = self.mounttab
124 v = table.get(None)
125 if v:
126 res.append(" " * len(indent) + repr(indent) + ": " + repr(v))
386c9c7c 127 for k, v in six.iteritems(table):
a2468d18
JW
128 if k == None:
129 continue
130 res.append(" " * len(indent) + repr(k) + ":")
131 res += self.__repr__(v, indent + [k])
132 if table is self.mounttab:
133 res.append(">")
134 return "\n".join(res)
135 else:
136 return res
137
138 def file_exists(self, filepath):
139 backend, filepath = self.resolve_to_backend(filepath)
140 return backend.file_exists(filepath)
141
142 def get_file(self, filepath, mode='r'):
143 backend, filepath = self.resolve_to_backend(filepath)
144 return backend.get_file(filepath, mode)
145
146 def delete_file(self, filepath):
147 backend, filepath = self.resolve_to_backend(filepath)
148 return backend.delete_file(filepath)
149
150 def file_url(self, filepath):
151 backend, filepath = self.resolve_to_backend(filepath)
152 return backend.file_url(filepath)
153
154 def get_local_path(self, filepath):
155 backend, filepath = self.resolve_to_backend(filepath)
156 return backend.get_local_path(filepath)
157
158 def copy_locally(self, filepath, dest_path):
159 """
160 Need to override copy_locally, because the local_storage
161 attribute is not correct.
162 """
163 backend, filepath = self.resolve_to_backend(filepath)
164 backend.copy_locally(filepath, dest_path)