| 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() |
|---|