chiark / gitweb /
Merge branch 'use_date_from_apk_in_known_apks' into 'master'
[fdroidserver.git] / fdroidserver / update.py
index 7cd18f415af28361d433cf02a8709dbc61b9baad..e4d2ff3c2e04e6eea956541aa33e526cfb021e5e 100644 (file)
@@ -323,8 +323,10 @@ def resize_icon(iconpath, density):
     if not os.path.isfile(iconpath):
         return
 
+    fp = None
     try:
-        im = Image.open(iconpath)
+        fp = open(iconpath, 'rb')
+        im = Image.open(fp)
         size = dpi_to_px(density)
 
         if any(length > size for length in im.size):
@@ -337,6 +339,10 @@ def resize_icon(iconpath, density):
     except Exception as e:
         logging.error("Failed resizing {0} - {1}".format(iconpath, e))
 
+    finally:
+        if fp:
+            fp.close()
+
 
 def resize_all_icons(repodirs):
     """Resize all icons that exceed the max size
@@ -405,6 +411,90 @@ def getsig(apkpath):
     return hashlib.md5(hexlify(cert_encoded)).hexdigest()
 
 
+def get_icon_bytes(apkzip, iconsrc):
+    '''ZIP has no official encoding, UTF-* and CP437 are defacto'''
+    try:
+        return apkzip.read(iconsrc)
+    except KeyError:
+        return apkzip.read(iconsrc.encode('utf-8').decode('cp437'))
+
+
+def sha256sum(filename):
+    '''Calculate the sha256 of the given file'''
+    sha = hashlib.sha256()
+    with open(filename, 'rb') as f:
+        while True:
+            t = f.read(16384)
+            if len(t) == 0:
+                break
+            sha.update(t)
+    return sha.hexdigest()
+
+
+def insert_obbs(repodir, apps, apks):
+    """Scans the .obb files in a given repo directory and adds them to the
+    relevant APK instances.  OBB files have versionCodes like APK
+    files, and they are loosely associated.  If there is an OBB file
+    present, then any APK with the same or higher versionCode will use
+    that OBB file.  There are two OBB types: main and patch, each APK
+    can only have only have one of each.
+
+    https://developer.android.com/google/play/expansion-files.html
+
+    :param repodir: repo directory to scan
+    :param apps: list of current, valid apps
+    :param apks: current information on all APKs
+
+    """
+
+    def obbWarnDelete(f, msg):
+        logging.warning(msg + f)
+        if options.delete_unknown:
+            logging.error("Deleting unknown file: " + f)
+            os.remove(f)
+
+    obbs = []
+    java_Integer_MIN_VALUE = -pow(2, 31)
+    for f in glob.glob(os.path.join(repodir, '*.obb')):
+        obbfile = os.path.basename(f)
+        # obbfile looks like: [main|patch].<expansion-version>.<package-name>.obb
+        chunks = obbfile.split('.')
+        if chunks[0] != 'main' and chunks[0] != 'patch':
+            obbWarnDelete(f, 'OBB filename must start with "main." or "patch.": ')
+            continue
+        if not re.match(r'^-?[0-9]+$', chunks[1]):
+            obbWarnDelete('The OBB version code must come after "' + chunks[0] + '.": ')
+            continue
+        versioncode = int(chunks[1])
+        packagename = ".".join(chunks[2:-1])
+
+        highestVersionCode = java_Integer_MIN_VALUE
+        if packagename not in apps.keys():
+            obbWarnDelete(f, "OBB's packagename does not match a supported APK: ")
+            continue
+        for apk in apks:
+            if packagename == apk['id'] and apk['versioncode'] > highestVersionCode:
+                highestVersionCode = apk['versioncode']
+        if versioncode > highestVersionCode:
+            obbWarnDelete(f, 'OBB file has newer versioncode(' + str(versioncode)
+                          + ') than any APK: ')
+            continue
+        obbsha256 = sha256sum(f)
+        obbs.append((packagename, versioncode, obbfile, obbsha256))
+
+    for apk in apks:
+        for (packagename, versioncode, obbfile, obbsha256) in sorted(obbs, reverse=True):
+            if versioncode <= apk['versioncode'] and packagename == apk['id']:
+                if obbfile.startswith('main.') and 'obbMainFile' not in apk:
+                    apk['obbMainFile'] = obbfile
+                    apk['obbMainFileSha256'] = obbsha256
+                elif obbfile.startswith('patch.') and 'obbPatchFile' not in apk:
+                    apk['obbPatchFile'] = obbfile
+                    apk['obbPatchFileSha256'] = obbsha256
+            if 'obbMainFile' in apk and 'obbPatchFile' in apk:
+                break
+
+
 def scan_apks(apps, apkcache, repodir, knownapks, use_date_from_apk=False):
     """Scan the apks in the given repo directory.
 
@@ -438,7 +528,7 @@ def scan_apks(apps, apkcache, repodir, knownapks, use_date_from_apk=False):
     icon_pat = re.compile(".*application-icon-([0-9]+):'([^']+?)'.*")
     icon_pat_nodpi = re.compile(".*icon='([^']+?)'.*")
     sdkversion_pat = re.compile(".*'([0-9]*)'.*")
-    string_pat = re.compile(".*'([^']*)'.*")
+    string_pat = re.compile(".* name='([^']*)'.*")
     for apkfile in glob.glob(os.path.join(repodir, '*.apk')):
 
         apkfilename = apkfile[len(repodir) + 1:]
@@ -446,15 +536,7 @@ def scan_apks(apps, apkcache, repodir, knownapks, use_date_from_apk=False):
             logging.critical("Spaces in filenames are not allowed.")
             sys.exit(1)
 
-        # Calculate the sha256...
-        sha = hashlib.sha256()
-        with open(apkfile, 'rb') as f:
-            while True:
-                t = f.read(16384)
-                if len(t) == 0:
-                    break
-                sha.update(t)
-            shasum = sha.hexdigest()
+        shasum = sha256sum(apkfile)
 
         usecache = False
         if apkfilename in apkcache:
@@ -563,7 +645,7 @@ def scan_apks(apps, apkcache, repodir, knownapks, use_date_from_apk=False):
 
             # Check for debuggable apks...
             if common.isApkDebuggable(apkfile, config):
-                logging.warn('{0} is set to android:debuggable="true"'.format(apkfile))
+                logging.warning('{0} is set to android:debuggable="true"'.format(apkfile))
 
             # Get the signature (or md5 of, to be precise)...
             logging.debug('Getting signature of {0}'.format(apkfile))
@@ -607,7 +689,7 @@ def scan_apks(apps, apkcache, repodir, knownapks, use_date_from_apk=False):
 
                 try:
                     with open(icondest, 'wb') as f:
-                        f.write(apkzip.read(iconsrc))
+                        f.write(get_icon_bytes(apkzip, iconsrc))
                     apk['icons'][density] = iconfilename
 
                 except:
@@ -621,7 +703,7 @@ def scan_apks(apps, apkcache, repodir, knownapks, use_date_from_apk=False):
                 iconpath = os.path.join(
                     get_icon_dir(repodir, '0'), iconfilename)
                 with open(iconpath, 'wb') as f:
-                    f.write(apkzip.read(iconsrc))
+                    f.write(get_icon_bytes(apkzip, iconsrc))
                 try:
                     im = Image.open(iconpath)
                     dpi = px_to_dpi(im.size[0])
@@ -657,17 +739,21 @@ def scan_apks(apps, apkcache, repodir, knownapks, use_date_from_apk=False):
                     get_icon_dir(repodir, last_density), iconfilename)
                 iconpath = os.path.join(
                     get_icon_dir(repodir, density), iconfilename)
+                fp = None
                 try:
-                    im = Image.open(last_iconpath)
-                except:
-                    logging.warn("Invalid image file at %s" % last_iconpath)
-                    continue
+                    fp = open(last_iconpath, 'rb')
+                    im = Image.open(fp)
 
-                size = dpi_to_px(density)
+                    size = dpi_to_px(density)
 
-                im.thumbnail((size, size), Image.ANTIALIAS)
-                im.save(iconpath, "PNG")
-                empty_densities.remove(density)
+                    im.thumbnail((size, size), Image.ANTIALIAS)
+                    im.save(iconpath, "PNG")
+                    empty_densities.remove(density)
+                except:
+                    logging.warning("Invalid image file at %s" % last_iconpath)
+                finally:
+                    if fp:
+                        fp.close()
 
             # Then just copy from the highest resolution available
             last_density = None
@@ -698,13 +784,14 @@ def scan_apks(apps, apkcache, repodir, knownapks, use_date_from_apk=False):
                 shutil.copyfile(baseline,
                                 os.path.join(get_icon_dir(repodir, '0'), iconfilename))
 
+            if use_date_from_apk and manifest.date_time[1] != 0:
+                default_date_param = datetime(*manifest.date_time).utctimetuple()
+            else:
+                default_date_param = None
+
             # Record in known apks, getting the added date at the same time..
-            added = knownapks.recordapk(apk['apkname'], apk['id'])
+            added = knownapks.recordapk(apk['apkname'], apk['id'], default_date=default_date_param)
             if added:
-                if use_date_from_apk and manifest.date_time[1] != 0:
-                    added = datetime(*manifest.date_time).timetuple()
-                    logging.debug("Using date from APK")
-
                 apk['added'] = added
 
             apkcache[apkfilename] = apk
@@ -953,6 +1040,10 @@ def make_index(apps, sortedids, apks, repodir, archive, categories):
                 addElement('targetSdkVersion', str(apk['targetSdkVersion']), doc, apkel)
             if 'maxSdkVersion' in apk:
                 addElement('maxsdkver', str(apk['maxSdkVersion']), doc, apkel)
+            addElementNonEmpty('obbMainFile', apk.get('obbMainFile'), doc, apkel)
+            addElementNonEmpty('obbMainFileSha256', apk.get('obbMainFileSha256'), doc, apkel)
+            addElementNonEmpty('obbPatchFile', apk.get('obbPatchFile'), doc, apkel)
+            addElementNonEmpty('obbPatchFileSha256', apk.get('obbPatchFileSha256'), doc, apkel)
             if 'added' in apk:
                 addElement('added', time.strftime('%Y-%m-%d', apk['added']), doc, apkel)
             addElementNonEmpty('permissions', ','.join(apk['permissions']), doc, apkel)
@@ -1138,7 +1229,7 @@ def main():
     parser.add_argument("-c", "--create-metadata", action="store_true", default=False,
                         help="Create skeleton metadata files that are missing")
     parser.add_argument("--delete-unknown", action="store_true", default=False,
-                        help="Delete APKs without metadata from the repo")
+                        help="Delete APKs and/or OBBs without metadata from the repo")
     parser.add_argument("-b", "--buildreport", action="store_true", default=False,
                         help="Report on build data status")
     parser.add_argument("-i", "--interactive", default=False, action="store_true",
@@ -1274,6 +1365,8 @@ def main():
     if newmetadata:
         apps = metadata.read_metadata()
 
+    insert_obbs(repodirs[0], apps, apks)
+
     # Scan the archive repo for apks as well
     if len(repodirs) > 1:
         archapks, cc = scan_apks(apps, apkcache, repodirs[1], knownapks, options.use_date_from_apk)