Ticket #1636: tmpSVjeVO

File tmpSVjeVO, 11.3 KB (added by martin.langhoff, 14 years ago)

sugar-1636-take2.patch

Line 
1diff --git a/src/jarabe/journal/listview.py b/src/jarabe/journal/listview.py
2index 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()
21diff --git a/src/jarabe/journal/model.py b/src/jarabe/journal/model.py
22index 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()