1 | diff --git a/src/jarabe/journal/listview.py b/src/jarabe/journal/listview.py |
---|
2 | index 6556b08..9b287c4 100644 |
---|
3 | --- a/src/jarabe/journal/listview.py |
---|
4 | +++ b/src/jarabe/journal/listview.py |
---|
5 | @@ -123,6 +123,15 @@ class BaseListView(gtk.HBox): |
---|
6 | datastore.connect_to_signal('Deleted', |
---|
7 | self.__datastore_deleted_cb) |
---|
8 | |
---|
9 | + model.created.connect(self.__model_updated_cb) |
---|
10 | + model.updated.connect(self.__model_updated_cb) |
---|
11 | + model.deleted.connect(self.__model_updated_cb) |
---|
12 | + |
---|
13 | + def __model_updated_cb(self, sender, **kwargs): |
---|
14 | + if 'object_id' in kwargs and kwargs['object_id'].startswith('/'): |
---|
15 | + # otherwise rely on ds' Deleted event for ds objects |
---|
16 | + self._set_dirty() |
---|
17 | + |
---|
18 | def __destroy_cb(self, widget): |
---|
19 | self._datastore_created_handler.remove() |
---|
20 | self._datastore_updated_handler.remove() |
---|
21 | diff --git a/src/jarabe/journal/model.py b/src/jarabe/journal/model.py |
---|
22 | index 9521456..b73e957 100644 |
---|
23 | --- a/src/jarabe/journal/model.py |
---|
24 | +++ b/src/jarabe/journal/model.py |
---|
25 | @@ -19,6 +19,7 @@ import os |
---|
26 | from datetime import datetime |
---|
27 | import time |
---|
28 | import shutil |
---|
29 | +import tempfile |
---|
30 | from stat import S_IFMT, S_IFDIR, S_IFREG |
---|
31 | import traceback |
---|
32 | import re |
---|
33 | @@ -27,6 +28,8 @@ import gobject |
---|
34 | import dbus |
---|
35 | import gconf |
---|
36 | import gio |
---|
37 | +import json |
---|
38 | +import base64 |
---|
39 | |
---|
40 | from sugar import dispatch |
---|
41 | from sugar import mime |
---|
42 | @@ -308,8 +311,10 @@ class InplaceResultSet(BaseResultSet): |
---|
43 | files = self._file_list[offset:offset + limit] |
---|
44 | |
---|
45 | entries = [] |
---|
46 | - for file_path, stat, mtime_ in files: |
---|
47 | - metadata = _get_file_metadata(file_path, stat) |
---|
48 | + for file_path, stat, mtime_, metadata in files: |
---|
49 | + if metadata == False: |
---|
50 | + # fallback - the find should fetch md |
---|
51 | + metadata = _get_file_metadata(file_path, stat) |
---|
52 | metadata['mountpoint'] = self._mount_point |
---|
53 | entries.append(metadata) |
---|
54 | |
---|
55 | @@ -334,9 +339,18 @@ class InplaceResultSet(BaseResultSet): |
---|
56 | elif S_IFMT(stat.st_mode) == S_IFREG: |
---|
57 | add_to_list = True |
---|
58 | |
---|
59 | - if self._regex is not None and \ |
---|
60 | - not self._regex.match(full_path): |
---|
61 | + md = _get_file_metadata_from_json(dir_path, entry, |
---|
62 | + preview=False) |
---|
63 | + if self._regex is not None: |
---|
64 | add_to_list = False |
---|
65 | + if self._regex.match(full_path): |
---|
66 | + add_to_list = True |
---|
67 | + elif md: |
---|
68 | + # match any of the text md fields |
---|
69 | + for f in ['fulltext', 'title', 'description', 'tags']: |
---|
70 | + if f in md and self._regex.match(md[f]): |
---|
71 | + add_to_list = True |
---|
72 | + break |
---|
73 | |
---|
74 | if None not in [self._date_start, self._date_end] and \ |
---|
75 | (stat.st_mtime < self._date_start or |
---|
76 | @@ -349,7 +363,7 @@ class InplaceResultSet(BaseResultSet): |
---|
77 | add_to_list = False |
---|
78 | |
---|
79 | if add_to_list: |
---|
80 | - file_info = (full_path, stat, int(stat.st_mtime)) |
---|
81 | + file_info = (full_path, stat, int(stat.st_mtime), md) |
---|
82 | self._file_list.append(file_info) |
---|
83 | |
---|
84 | self.progress.send(self) |
---|
85 | @@ -364,6 +378,15 @@ class InplaceResultSet(BaseResultSet): |
---|
86 | self._pending_directories -= 1 |
---|
87 | |
---|
88 | def _get_file_metadata(path, stat): |
---|
89 | + # will sometimes be called for |
---|
90 | + # files that do have a metatada file |
---|
91 | + fname = os.path.basename(path) |
---|
92 | + dir_path = os.path.dirname(path) |
---|
93 | + md = _get_file_metadata_from_json(dir_path, fname, preview=True) |
---|
94 | + if md: |
---|
95 | + return md |
---|
96 | + |
---|
97 | + # make up something for files w/o metadata |
---|
98 | client = gconf.client_get_default() |
---|
99 | return {'uid': path, |
---|
100 | 'title': os.path.basename(path), |
---|
101 | @@ -374,6 +397,30 @@ def _get_file_metadata(path, stat): |
---|
102 | 'icon-color': client.get_string('/desktop/sugar/user/color'), |
---|
103 | 'description': path} |
---|
104 | |
---|
105 | +def _get_file_metadata_from_json(dir_path, fname, preview=False): |
---|
106 | + md = None |
---|
107 | + mdpath = os.path.join(dir_path, |
---|
108 | + '.'+fname+'.metadata') |
---|
109 | + md = False |
---|
110 | + if os.path.exists(mdpath): |
---|
111 | + try: |
---|
112 | + md = json.load(open(mdpath)) |
---|
113 | + md['uid'] = os.path.join(dir_path, fname) |
---|
114 | + except: |
---|
115 | + pass |
---|
116 | + if preview: |
---|
117 | + prpath = os.path.join(dir_path, |
---|
118 | + '.'+fname+'.preview') |
---|
119 | + try: |
---|
120 | + preview = base64.b64encode(open(prpath).read()) |
---|
121 | + md['preview'] = preview |
---|
122 | + except: |
---|
123 | + pass |
---|
124 | + else: |
---|
125 | + if md and 'preview' in md: |
---|
126 | + del(md['preview']) |
---|
127 | + return md |
---|
128 | + |
---|
129 | _datastore = None |
---|
130 | def _get_datastore(): |
---|
131 | global _datastore |
---|
132 | @@ -460,6 +507,18 @@ def delete(object_id): |
---|
133 | """ |
---|
134 | if os.path.exists(object_id): |
---|
135 | os.unlink(object_id) |
---|
136 | + dir_path = os.path.dirname(object_id) |
---|
137 | + fname = os.path.basename(object_id) |
---|
138 | + old_files = [ os.path.join(dir_path, |
---|
139 | + '.'+fname+'.metadata'), |
---|
140 | + os.path.join(dir_path, |
---|
141 | + '.'+fname+'.preview') ] |
---|
142 | + for ofile in old_files: |
---|
143 | + if os.path.exists(ofile): |
---|
144 | + try: |
---|
145 | + os.unlink(ofile) |
---|
146 | + except: |
---|
147 | + pass |
---|
148 | deleted.send(None, object_id=object_id) |
---|
149 | else: |
---|
150 | _get_datastore().delete(object_id) |
---|
151 | @@ -480,6 +539,7 @@ def write(metadata, file_path='', update_mtime=True): |
---|
152 | """ |
---|
153 | logging.debug('model.write %r %r %r' % (metadata.get('uid', ''), file_path, |
---|
154 | update_mtime)) |
---|
155 | + |
---|
156 | if update_mtime: |
---|
157 | metadata['mtime'] = datetime.now().isoformat() |
---|
158 | metadata['timestamp'] = int(time.time()) |
---|
159 | @@ -495,16 +555,87 @@ def write(metadata, file_path='', update_mtime=True): |
---|
160 | file_path, |
---|
161 | True) |
---|
162 | else: |
---|
163 | - if not os.path.exists(file_path): |
---|
164 | + if 'uid' in metadata and os.path.exists(metadata['uid']): |
---|
165 | + file_path = metadata['uid'] |
---|
166 | + |
---|
167 | + if not file_path or not os.path.exists(file_path): |
---|
168 | raise ValueError('Entries without a file cannot be copied to ' |
---|
169 | 'removable devices') |
---|
170 | |
---|
171 | + # cases for file_name: |
---|
172 | + # - metadata update -> keep same fname |
---|
173 | + # - new file / copy from DS -> check for name collision |
---|
174 | + # - renaming file on external storage -> check for collision |
---|
175 | file_name = _get_file_name(metadata['title'], metadata['mime_type']) |
---|
176 | - file_name = _get_unique_file_name(metadata['mountpoint'], file_name) |
---|
177 | - |
---|
178 | destination_path = os.path.join(metadata['mountpoint'], file_name) |
---|
179 | - shutil.copy(file_path, destination_path) |
---|
180 | + if destination_path != file_path: |
---|
181 | + # new file (copied from DS) or rename |
---|
182 | + if os.path.exists(destination_path): |
---|
183 | + # diverting to a new filename to avoid collision |
---|
184 | + file_name = _get_unique_file_name(metadata['mountpoint'], file_name) |
---|
185 | + destination_path = os.path.join(metadata['mountpoint'], file_name) |
---|
186 | + # reflect this in title - to avoid misleading titles in ext media |
---|
187 | + cleanname, extension = os.path.splitext(file_name) |
---|
188 | + metadata['title'] = cleanname |
---|
189 | + |
---|
190 | + ## Prepare and write the metadata out first |
---|
191 | + ## this is sane for new objects (copy from DS) |
---|
192 | + ## and renames. Additionally, it makes renames |
---|
193 | + ## failsafe. |
---|
194 | + |
---|
195 | + # issue our metadata massage on a copy |
---|
196 | + md = metadata.copy() |
---|
197 | + del md['mountpoint'] |
---|
198 | + |
---|
199 | + # if we ever write to places other than the root |
---|
200 | + # keep in mind that the metadata file must be in the |
---|
201 | + # same directory as the data file. |
---|
202 | + md_path = os.path.join(metadata['mountpoint'], |
---|
203 | + '.'+file_name+'.metadata') |
---|
204 | + |
---|
205 | + if 'preview' in md: |
---|
206 | + preview = md['preview'] |
---|
207 | + preview_fname = '.'+file_name+'.preview' |
---|
208 | + preview_path = os.path.join(metadata['mountpoint'], preview_fname) |
---|
209 | + md['preview'] = preview_fname |
---|
210 | + |
---|
211 | + # Write preview atomically |
---|
212 | + (fh, fn) = tempfile.mkstemp(dir=metadata['mountpoint']) |
---|
213 | + os.write(fh, preview) |
---|
214 | + os.close(fh) |
---|
215 | + os.rename(fn, preview_path) |
---|
216 | + |
---|
217 | + # Write metadata atomically (on FSs that support it) |
---|
218 | + (fh, fn) = tempfile.mkstemp(dir=metadata['mountpoint']) |
---|
219 | + os.write(fh, json.dumps(md)) |
---|
220 | + os.close(fh) |
---|
221 | + os.rename(fn, md_path) |
---|
222 | + |
---|
223 | + # Now deal with the actual file |
---|
224 | + if os.path.dirname(destination_path) == os.path.dirname(file_path): |
---|
225 | + # same dir case - updated metadata, or rename |
---|
226 | + old_file_path = file_path |
---|
227 | + if old_file_path != destination_path: |
---|
228 | + # title change leads to rename on removable disk |
---|
229 | + os.rename(file_path, destination_path) |
---|
230 | + old_fname = os.path.basename(file_path) |
---|
231 | + old_files = [ os.path.join(metadata['mountpoint'], |
---|
232 | + '.'+old_fname+'.metadata'), |
---|
233 | + os.path.join(metadata['mountpoint'], |
---|
234 | + '.'+old_fname+'.preview') ] |
---|
235 | + for ofile in old_files: |
---|
236 | + if os.path.exists(ofile): |
---|
237 | + try: |
---|
238 | + os.unlink(ofile) |
---|
239 | + except: |
---|
240 | + pass |
---|
241 | + else: |
---|
242 | + # different dir - likely copy from DS |
---|
243 | + shutil.copy(file_path, destination_path) |
---|
244 | + |
---|
245 | + |
---|
246 | object_id = destination_path |
---|
247 | + metadata['uid'] = object_id |
---|
248 | created.send(None, object_id=object_id) |
---|
249 | |
---|
250 | return object_id |
---|
251 | @@ -512,9 +643,11 @@ def write(metadata, file_path='', update_mtime=True): |
---|
252 | def _get_file_name(title, mime_type): |
---|
253 | file_name = title |
---|
254 | |
---|
255 | - extension = '.' + mime.get_primary_extension(mime_type) |
---|
256 | - if not file_name.endswith(extension): |
---|
257 | - file_name += extension |
---|
258 | + mime_extension = mime.get_primary_extension(mime_type) |
---|
259 | + if mime_extension: |
---|
260 | + extension = '.' + mime_extension |
---|
261 | + if not file_name.endswith(extension): |
---|
262 | + file_name += extension |
---|
263 | |
---|
264 | # Invalid characters in VFAT filenames. From |
---|
265 | # http://en.wikipedia.org/wiki/File_Allocation_Table |
---|
266 | @@ -534,14 +667,14 @@ def _get_file_name(title, mime_type): |
---|
267 | def _get_unique_file_name(mount_point, file_name): |
---|
268 | if os.path.exists(os.path.join(mount_point, file_name)): |
---|
269 | i = 1 |
---|
270 | + name, extension = os.path.splitext(file_name) |
---|
271 | while len(file_name) <= 255: |
---|
272 | - name, extension = os.path.splitext(file_name) |
---|
273 | - file_name = name + '_' + str(i) + extension |
---|
274 | - if not os.path.exists(os.path.join(mount_point, file_name)): |
---|
275 | + try_file_name = name + '_' + str(i) + extension |
---|
276 | + if not os.path.exists(os.path.join(mount_point, try_file_name)): |
---|
277 | break |
---|
278 | i += 1 |
---|
279 | |
---|
280 | - return file_name |
---|
281 | + return try_file_name |
---|
282 | |
---|
283 | created = dispatch.Signal() |
---|
284 | updated = dispatch.Signal() |
---|