Index: ow/models/bulk.py
===================================================================
--- ow/models/bulk.py	(revision fcf0ef3d4ad92cfddebb4201d049a17183adb13f)
+++ ow/models/bulk.py	(revision 715671fdf99570e39696f9018ac19983dd436b04)
@@ -1,9 +1,18 @@
+import os
+import logging
+from shutil import unpack_archive, rmtree
 from uuid import uuid1
-from datetime import datetime, timedelta, timezone
-
+from datetime import datetime, timezone
+
+import pytz
 from repoze.folder import Folder
 from pyramid.security import Allow, Deny, Everyone, ALL_PERMISSIONS
-
-import pytz
+from fitparse.utils import FitHeaderError, FitEOFError
+from gpxpy.gpx import GPXXMLSyntaxException
+
+from ow.utilities import create_blob
+from ow.models.workout import Workout
+
+log = logging.getLogger(__name__)
 
 
@@ -40,4 +49,5 @@
         self.loaded = False  # datetime when workouts have been loaded
         self.workout_ids = []  # ids of the workouts loaded from this file
+        self.loaded_info = {}  # per-file information (loaded or not, errors..)
 
     def _in_timezone(self, timezone, value):
@@ -56,4 +66,108 @@
             return self._in_timezone(timezone, self.loaded)
         return ''
+
+    def extract(self, tmp_path, path):
+        """
+        Extract the files contained in this bulk/compressed file into the
+        given path.
+        """
+        if self.compressed_file is None:
+            return []
+
+        # save the blob into a temporal file
+        tmp_file_path = os.path.join(tmp_path, self.file_name)
+        with open(tmp_file_path, 'wb') as tmp_file:
+            with self.compressed_file.open() as blob:
+                tmp_file.write(blob.read())
+        # extract
+        unpack_archive(tmp_file_path, path)
+        # remove temporary file
+        os.remove(tmp_file_path)
+        # analyze the extracted contents, return some data
+        extracted = []
+        if os.path.exists(path):
+            for extracted_file in os.listdir(path):
+                extracted.append(os.path.join(path, extracted_file))
+        return extracted
+
+    def load(self, root, tmp_path):
+        user = root.get_user_by_uid(self.uid)
+        # extract
+        tmp_extract_path = os.path.join(tmp_path, str(self.bfid))
+        log.info(self.file_name + ' extracting to ' + tmp_extract_path)
+        extracted = self.extract(tmp_path, tmp_extract_path)
+        log.info(self.file_name + ' ' + str(len(extracted)) +
+                 ' files extracted')
+
+        # loop over extracted files and create the workouts, taking
+        # care of duplicates. Store some stats/info in a dict, so we can
+        # keep that somewhere, to show to the user later on
+        for extracted_file in extracted:
+            base_file_name = os.path.basename(extracted_file)
+            log_header = self.file_name + '/' + base_file_name
+            log.info(log_header + ' loading file')
+
+            file_extension = os.path.splitext(base_file_name)[1].strip('.')
+
+            # gpx files are text, but fit files are binary files
+            open_mode = 'r'
+            binary = False
+            if file_extension == 'fit':
+                open_mode = 'rb'
+                binary = True
+
+            with open(extracted_file, open_mode) as f_obj:
+                blob = create_blob(
+                    f_obj.read(), file_extension=file_extension, binary=binary)
+
+            workout = Workout()
+            workout.tracking_file = blob
+            workout.tracking_filetype = file_extension
+
+            try:
+                workout.load_from_file()
+            except (FitHeaderError, FitEOFError, GPXXMLSyntaxException) as e:
+                log.error(log_header + ' error loading tracking file ')
+                log.error(e)
+                self.loaded_info[base_file_name] = {
+                    'loaded': False,
+                    'error': 'tracking file load error',
+                    'workout': None,
+                }
+            else:
+                # check for duplicates
+                # hashed is not "complete" for a workout that has not been
+                # added yet, as it does not have the owner set, so we have to
+                # "build it"
+                hashed = str(self.uid) + workout.hashed
+                duplicate = root.get_workout_by_hash(hashed)
+                if duplicate:
+                    log.warning(
+                        log_header +
+                        ' cannot create workout, possible duplicate')
+                    self.loaded_info[base_file_name] = {
+                        'loaded': False,
+                        'error': 'Possible duplicate workout',
+                        'workout': None,
+                    }
+                else:
+                    # add the workout only if no errors happened
+                    user.add_workout(workout)
+                    log.info(log_header + ' workout added')
+                    self.loaded_info[base_file_name] = {
+                        'loaded': True,
+                        'error': None,
+                        'workout': workout.workout_id,
+                    }
+                    self.workout_ids.append(workout.workout_id)
+
+        # clean-up, we have to check if the temporary directory exists,
+        # as extract() won't create such directory if the compressed file
+        # is empty
+        if os.path.exists(tmp_extract_path):
+            rmtree(tmp_extract_path)
+
+        # mark this bulk file as loaded
+        self.loaded = datetime.now(timezone.utc)
 
 
