Commit | Line | Data |
---|---|---|
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 |
17 | from __future__ import print_function |
18 | ||
386c9c7c BP |
19 | import six |
20 | ||
dd1756ee | 21 | from mediagoblin.storage import StorageInterface, clean_listy_filepath |
a2468d18 JW |
22 | |
23 | ||
a855e92a WKG |
24 | class MountError(Exception): |
25 | pass | |
26 | ||
27 | ||
a2468d18 JW |
28 | class 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) |