diff options
Diffstat (limited to 'datastore/src/carquinyol/filestore.py')
-rw-r--r-- | datastore/src/carquinyol/filestore.py | 222 |
1 files changed, 222 insertions, 0 deletions
diff --git a/datastore/src/carquinyol/filestore.py b/datastore/src/carquinyol/filestore.py new file mode 100644 index 0000000..9724397 --- /dev/null +++ b/datastore/src/carquinyol/filestore.py @@ -0,0 +1,222 @@ +# Copyright (C) 2008, One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import errno +import logging +import tempfile + +import gobject + +from carquinyol import layoutmanager + + +class FileStore(object): + """Handle the storage of one file per entry. + """ + + # TODO: add protection against store and retrieve operations on entries + # that are being processed async. + + def store(self, uid, file_path, transfer_ownership, completion_cb): + """Store a file for a given entry. + + """ + dir_path = layoutmanager.get_instance().get_entry_path(uid) + if not os.path.exists(dir_path): + os.makedirs(dir_path) + + destination_path = layoutmanager.get_instance().get_data_path(uid) + if file_path: + if not os.path.isfile(file_path): + raise ValueError('No file at %r' % file_path) + if transfer_ownership: + try: + logging.debug('FileStore moving from %r to %r', file_path, + destination_path) + os.rename(file_path, destination_path) + completion_cb() + except OSError, e: + if e.errno == errno.EXDEV: + self._async_copy(file_path, destination_path, + completion_cb, unlink_src=True) + else: + raise + else: + self._async_copy(file_path, destination_path, completion_cb, + unlink_src=False) + """ + TODO: How can we support deleting the file of an entry? + elif not file_path and os.path.exists(destination_path): + logging.debug('FileStore: deleting %r' % destination_path) + os.remove(destination_path) + completion_cb() + """ + else: + logging.debug('FileStore: Nothing to do') + completion_cb() + + def _async_copy(self, file_path, destination_path, completion_cb, + unlink_src): + """Start copying a file asynchronously. + + """ + logging.debug('FileStore copying from %r to %r', file_path, + destination_path) + async_copy = AsyncCopy(file_path, destination_path, completion_cb, + unlink_src) + async_copy.start() + + def retrieve(self, uid, user_id, extension): + """Place the file associated to a given entry into a directory + where the user can read it. The caller is reponsible for + deleting this file. + + """ + file_path = layoutmanager.get_instance().get_data_path(uid) + if not os.path.exists(file_path): + logging.debug('Entry %r doesnt have any file', uid) + return '' + + use_instance_dir = os.path.exists('/etc/olpc-security') and \ + os.getuid() != user_id + if use_instance_dir: + if not user_id: + raise ValueError('Couldnt determine the current user uid.') + destination_dir = os.path.join(os.environ['HOME'], 'isolation', + '1', 'uid_to_instance_dir', str(user_id)) + else: + profile = os.environ.get('SUGAR_PROFILE', 'default') + destination_dir = os.path.join(os.path.expanduser('~'), '.sugar', + profile, 'data') + if not os.path.exists(destination_dir): + os.makedirs(destination_dir) + + if extension is None: + extension = '' + elif extension: + extension = '.' + extension + + fd, destination_path = tempfile.mkstemp(prefix=uid + '_', + suffix=extension, dir=destination_dir) + os.close(fd) + os.unlink(destination_path) + + # Try to hard link from the original file to the targetpath. This can + # fail if the file is in a different filesystem. Do a symlink instead. + try: + os.link(file_path, destination_path) + except OSError, e: + if e.errno == errno.EXDEV: + os.symlink(file_path, destination_path) + else: + raise + + # Try to make the original file readable. This can fail if the file is + # in a FAT filesystem. + try: + os.chmod(file_path, 0604) + except OSError, e: + if e.errno != errno.EPERM: + raise + + return destination_path + + def get_file_path(self, uid): + return layoutmanager.get_instance().get_data_path(uid) + + def delete(self, uid): + """Remove the file associated to a given entry. + + """ + file_path = layoutmanager.get_instance().get_data_path(uid) + if os.path.exists(file_path): + os.remove(file_path) + + def hard_link_entry(self, new_uid, existing_uid): + existing_file = layoutmanager.get_instance().get_data_path(existing_uid) + new_file = layoutmanager.get_instance().get_data_path(new_uid) + + logging.debug('removing %r', new_file) + os.remove(new_file) + + logging.debug('hard linking %r -> %r', new_file, existing_file) + os.link(existing_file, new_file) + + +class AsyncCopy(object): + """Copy a file in chunks in the idle loop. + + """ + CHUNK_SIZE = 65536 + + def __init__(self, src, dest, completion, unlink_src=False): + self.src = src + self.dest = dest + self.completion = completion + self._unlink_src = unlink_src + self.src_fp = -1 + self.dest_fp = -1 + self.written = 0 + self.size = 0 + + def _cleanup(self): + os.close(self.src_fp) + os.close(self.dest_fp) + + def _copy_block(self, user_data=None): + try: + data = os.read(self.src_fp, AsyncCopy.CHUNK_SIZE) + count = os.write(self.dest_fp, data) + self.written += len(data) + + # error writing data to file? + if count < len(data): + logging.error('AC: Error writing %s -> %s: wrote less than ' + 'expected', self.src, self.dest) + self._complete(RuntimeError( + 'Error writing data to destination file')) + return False + + # FIXME: emit progress here + + # done? + if len(data) < AsyncCopy.CHUNK_SIZE: + self._complete(None) + return False + except Exception, err: + logging.error('AC: Error copying %s -> %s: %r', self.src, self. + dest, err) + self._complete(err) + return False + + return True + + def _complete(self, *args): + self._cleanup() + if self._unlink_src: + os.unlink(self.src) + self.completion(*args) + + def start(self): + self.src_fp = os.open(self.src, os.O_RDONLY) + self.dest_fp = os.open(self.dest, os.O_RDWR | os.O_TRUNC | os.O_CREAT, + 0644) + + stat = os.fstat(self.src_fp) + self.size = stat[6] + + gobject.idle_add(self._copy_block) |