diff options
author | Nikolay Zamotaev <nzamotaev@luxoft.com> | 2020-02-20 17:13:17 +0300 |
---|---|---|
committer | Nikolay Zamotaev <nzamotaev@luxoft.com> | 2020-08-18 15:34:02 +0000 |
commit | 23450bceb6b9d032b7444ab25947d405dcb441d8 (patch) | |
tree | a297c24bb6cd90ce80903c64a21b1d5f230756fe | |
parent | 4f9f1fc171601d151ee0208efbfd0ffa9e73b501 (diff) |
Implement reverse tag matchingv5.15.0_QtAS
Fixes: AUTOSUITE-1447
Change-Id: I7f8c3c83d49939962376c98a650b4ecb24f8e980
Reviewed-by: Nikolay Zamotaev <nzamotaev@luxoft.com>
-rw-r--r-- | appstore/settings.py | 4 | ||||
-rw-r--r-- | appstore/urls.py | 7 | ||||
-rw-r--r-- | appstore/wsgi.py | 3 | ||||
-rw-r--r-- | doc/src/deployment-server-reference.qdoc | 28 | ||||
-rw-r--r-- | store/admin.py | 59 | ||||
-rw-r--r-- | store/api.py | 204 | ||||
-rw-r--r-- | store/management/commands/store-upload-package.py | 31 | ||||
-rw-r--r-- | store/migrations/0001_initial.py | 26 | ||||
-rw-r--r-- | store/models.py | 111 | ||||
-rw-r--r-- | store/osandarch.py | 54 | ||||
-rw-r--r-- | store/tags.py | 131 | ||||
-rw-r--r-- | store/utilities.py | 35 |
12 files changed, 421 insertions, 272 deletions
diff --git a/appstore/settings.py b/appstore/settings.py index 092ea09..b03e943 100644 --- a/appstore/settings.py +++ b/appstore/settings.py @@ -39,6 +39,7 @@ https://docs.djangoproject.com/en/1.7/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/1.7/ref/settings/ """ +import os APPSTORE_MAINTENANCE = False APPSTORE_PLATFORM_ID = 'NEPTUNE3' @@ -50,10 +51,9 @@ APPSTORE_BIND_TO_DEVICE_ID = True # unique downloads for each device APPSTORE_NO_SECURITY = True # ignore developer signatures and do not generate store signatures APPSTORE_STORE_SIGN_PKCS12_CERTIFICATE = 'certificates/store.p12' APPSTORE_STORE_SIGN_PKCS12_PASSWORD = 'password' -APPSTORE_DEV_VERIFY_CA_CERTIFICATES = [ 'certificates/ca.crt', 'certificates/devca.crt' ] +APPSTORE_DEV_VERIFY_CA_CERTIFICATES = ['certificates/ca.crt', 'certificates/devca.crt'] # Build paths inside the project like this: os.path.join(BASE_DIR, ...) -import os BASE_DIR = os.path.dirname(os.path.dirname(__file__)) diff --git a/appstore/urls.py b/appstore/urls.py index 7c99a6a..1eb3d2c 100644 --- a/appstore/urls.py +++ b/appstore/urls.py @@ -33,7 +33,7 @@ from django.conf.urls import include, url from django.contrib import admin from store import api as store_api -import settings +from appstore.settings import URL_PREFIX base_urlpatterns = [ url(r'^admin/', include(admin.site.urls)), @@ -42,6 +42,7 @@ base_urlpatterns = [ url(r'^login$', store_api.login), url(r'^logout$', store_api.logout), url(r'^app/list$', store_api.appList), + url(r'^app/icons/(.*)$', store_api.appIconNew), url(r'^app/icon', store_api.appIcon), url(r'^app/description', store_api.appDescription), url(r'^app/purchase', store_api.appPurchase), @@ -53,8 +54,8 @@ base_urlpatterns = [ prefix = '^' -if settings.URL_PREFIX !='': - prefix = prefix + settings.URL_PREFIX + '/' +if URL_PREFIX != '': + prefix = prefix + URL_PREFIX + '/' urlpatterns = [ url(prefix, include(base_urlpatterns)), diff --git a/appstore/wsgi.py b/appstore/wsgi.py index 3f007d4..5ff13a6 100644 --- a/appstore/wsgi.py +++ b/appstore/wsgi.py @@ -40,7 +40,8 @@ https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ """ import os +from django.core.wsgi import get_wsgi_application + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "appstore.settings") -from django.core.wsgi import get_wsgi_application application = get_wsgi_application() diff --git a/doc/src/deployment-server-reference.qdoc b/doc/src/deployment-server-reference.qdoc index 1ade88f..a8903de 100644 --- a/doc/src/deployment-server-reference.qdoc +++ b/doc/src/deployment-server-reference.qdoc @@ -55,15 +55,10 @@ \li The Deployment Server's HTTP API version that you are using to communicate with the server. For more information, see \c{settings.APPSTORE_VERSION}. \row - \li require_tag + \li require_tag (can also be passed as \c{tag} ) \li An optional parameter used to filter packages by tags. Receives a comma-separated - list of tags; these tags must be alphanumeric. Only applications that contain any - of the specified tags should be listed. - \row - \li conflicts_tag - \li An optional parameter used to filter packages by tags. Receives a comma-separated - list of tags; these tags must be alphanumeric. Applications that contain any of the - speicifed tags should be excluded. + list of tags; these tags must be alphanumeric. Only applications that match any of the + specified tags (and not matching those in their conflicts list should be listed). \row \li architecture \li An optional parameter used to filter packages by architecture. Receives the CPU @@ -190,7 +185,10 @@ \li A category name for the app. \row \li tags - \li JSON array of app tags + \li JSON array of tags required for the app + \row + \li conflict_tags + \li JSON array of tags which will conflict with the app \row \li version \li The app's version, returned as a string. If the there is no version number, the @@ -216,6 +214,14 @@ \row \li category_id \li Numeric category ID that matches the app's category. + \row + \li purchaseId + \li Purchase identifier, freeform identifier of specific application on the server. + Used as an alternative to ID in \c{app/purchase} API call. This is a freeform string, + currently a UUID, but no assumptions should be made of the format. + \row + \li iconUrl + \li URL for the icon of this application \endtable \section2 app/icon @@ -270,6 +276,10 @@ \row \li id \li The app ID. + \row + \li purchaseId + \li Alternative app ID, to select specific app with tags and all (see \c{app/list} API description). + If both ID and purchaseId are specified, ID takes precedence. \endtable Returns a JSON object: \table diff --git a/store/admin.py b/store/admin.py index 2252af1..c55f6d4 100644 --- a/store/admin.py +++ b/store/admin.py @@ -30,7 +30,6 @@ ## ############################################################################# -import os import StringIO from PIL import Image, ImageChops @@ -42,7 +41,7 @@ from django.core.files.uploadedfile import InMemoryUploadedFile from ordered_model.admin import OrderedModelAdmin from store.models import * -from utilities import parseAndValidatePackageMetadata, writeTempIcon, makeTagList +from store.utilities import parseAndValidatePackageMetadata, writeTempIcon, makeTagList class CategoryAdminForm(forms.ModelForm): class Meta: @@ -60,14 +59,14 @@ class CategoryAdminForm(forms.ModelForm): # make image monochrome + alpha channel, this is done to compensate for # how icons are treated in neptune3-ui im = Image.open(cleaned_data['icon']) - grey, alpha = im.convert('LA').split() + grey, _ = im.convert('LA').split() grey = ImageChops.invert(grey) im.putalpha(grey) im = im.convert('LA') else: # No conversion, icons are uploaded as-is, only scaling is used. im = Image.open(cleaned_data['icon']) - size = (settings.ICON_SIZE_X,settings.ICON_SIZE_Y,) + size = (settings.ICON_SIZE_X, settings.ICON_SIZE_Y) im.thumbnail(size, Image.ANTIALIAS) imagefile = StringIO.StringIO() im.save(imagefile, format='png') @@ -100,41 +99,48 @@ class CategoryAdmin(OrderedModelAdmin): class AppAdminForm(forms.ModelForm): class Meta: - exclude = ["appid", "name", "tags", "architecture", 'version', 'pkgformat'] + exclude = ["appid", "name", "tags", "tags_hash", "architecture", 'version', 'pkgformat'] appId = "" name = "" def clean(self): cleaned_data = super(AppAdminForm, self).clean() - file = cleaned_data.get('file') + package_file = cleaned_data.get('file') # validate package try: - pkgdata = parseAndValidatePackageMetadata(file) + pkgdata = parseAndValidatePackageMetadata(package_file) except Exception as error: raise forms.ValidationError(_('Validation error: %s' % str(error))) self.appId = pkgdata['info']['id'] self.name = pkgdata['storeName'] self.architecture = pkgdata['architecture'] - self.tags = makeTagList(pkgdata) + require_tags, conflict_tags, self.tags_hash = makeTagList(pkgdata) # check if this really is an update if hasattr(self, 'instance') and self.instance.appid: if self.appId != self.instance.appid: - raise forms.ValidationError(_('Validation error: an update cannot change the application id, tried to change from %s to %s' % (self.instance.appid, self.appId))) + raise forms.ValidationError(_('Validation error: an update cannot change the ' + 'application id, tried to change from %s to %s' % + (self.instance.appid, self.appId))) elif self.architecture != self.instance.architecture: - raise forms.ValidationError(_('Validation error: an update cannot change the application architecture from %s to %s' % (self.instance.architecture, self.architecture))) + raise forms.ValidationError(_('Validation error: an update cannot change the ' + 'application architecture from %s to %s' % + (self.instance.architecture, self.architecture))) else: try: - if App.objects.get(appid__exact = self.appId, architecture__exact = self.architecture, tags__exact = self.tags): - raise forms.ValidationError(_('Validation error: another application with id %s , tags %s and architecture %s already exists' % (str(self.appId), str(self.tags), str(self.architecture)))) + if App.objects.get(appid__exact=self.appId, architecture__exact=self.architecture, tags_hash__exact=self.tags_hash): + raise forms.ValidationError(_('Validation error: another application with id' + ' %s , tags %s and architecture %s already ' + 'exists' % (str(self.appId), str(self.tags_hash), + str(self.architecture)))) except App.DoesNotExist: pass # write icon into file to serve statically - success, error = writeTempIcon(self.appId, self.architecture, self.tags, pkgdata['icon']) + success, error = writeTempIcon(self.appId, self.architecture, self.tags_hash, pkgdata['icon']) if not success: raise forms.ValidationError(_(error)) @@ -148,19 +154,42 @@ class AppAdminForm(forms.ModelForm): m.file.seek(0) pkgdata = parseAndValidatePackageMetadata(m.file) - m.tags = makeTagList(pkgdata) + tags, conflict_tags, m.tags_hash = makeTagList(pkgdata) + + # FIXME - add tags? + taglist = populateTagList(tags.list(),conflict_tags.list()) + + # FIXME: clean tags beforehand m.pkgformat = pkgdata['packageFormat']['formatVersion'] + m.save() + + for i in taglist: + # attach tags to app + m.tags.add(i) + return m class AppAdmin(admin.ModelAdmin): form = AppAdminForm - list_display = ('name', 'appid', 'architecture', 'version', 'pkgformat', 'tags') + list_display = ('name', 'appid', 'architecture', 'version', 'pkgformat', 'tags_hash') def save_model(self, request, obj, form, change): obj.save() +class TagAdmin(admin.ModelAdmin): + list_display = ('negative', 'name', 'version') + + def has_add_permission(self, request, obj=None): + return False + + def has_delete_permission(self, request, obj=None): + return False + + def save_model(self, request, obj, form, change): + obj.save() admin.site.register(Category, CategoryAdmin) admin.site.register(Vendor) +admin.site.register(Tag, TagAdmin) admin.site.register(App, AppAdmin) diff --git a/store/api.py b/store/api.py index fe8e25d..75e730f 100644 --- a/store/api.py +++ b/store/api.py @@ -39,14 +39,16 @@ from django.db.models import Q, Count from django.http import HttpResponse, HttpResponseForbidden, Http404, JsonResponse from django.contrib import auth from django.views.decorators.csrf import csrf_exempt -from authdecorators import logged_in_or_basicauth, is_staff_member +from django.core.exceptions import ValidationError +from store.authdecorators import logged_in_or_basicauth, is_staff_member -from models import App, Category, Vendor, savePackageFile -from utilities import parsePackageMetadata, parseAndValidatePackageMetadata, addSignatureToPackage -from utilities import packagePath, iconPath, downloadPath -from utilities import getRequestDictionary -from osandarch import normalizeArch -from tags import SoftwareTagList +from store.models import App, Category, Vendor, savePackageFile +from store.utilities import parsePackageMetadata, parseAndValidatePackageMetadata, \ + addSignatureToPackage +from store.utilities import packagePath, iconPath, downloadPath +from store.utilities import getRequestDictionary +from store.osandarch import normalizeArch +from store.tags import SoftwareTagList def hello(request): @@ -66,16 +68,21 @@ def hello(request): elif not ((version) > 0 and (version <= settings.APPSTORE_PLATFORM_VERSION)): status = 'incompatible-version' - for j in ("require_tag", "conflicts_tag",): - if j in dictionary: #Tags are coma-separated, - versionmap = SoftwareTagList() - if not versionmap.parse(getRequestDictionary(request)[j]): - status = 'malformed-tag' - break - request.session[j] = str(versionmap) + # Tag parsing + temp_tags = [] + for j in ("require_tag", "tag"): + if j in dictionary: + temp_tags.append(dictionary[j]) + versionmap = SoftwareTagList() + #Tags are coma-separated, - so join them + if temp_tags: + if not versionmap.parse(','.join(temp_tags)): + status = 'malformed-tag' + request.session["tag"] = str(versionmap) + del temp_tags if 'architecture' in dictionary: - arch = normalizeArch(getRequestDictionary(request)['architecture']) + arch = normalizeArch(dictionary['architecture']) if arch == "": status = 'incompatible-architecture' request.session['architecture'] = arch @@ -95,7 +102,7 @@ def login(request): except KeyError: raise Exception('missing-credentials') - user = auth.authenticate(username = username, password = password) + user = auth.authenticate(username=username, password=password) if user is None: raise Exception('authentication-failed') @@ -147,9 +154,9 @@ def upload(request): myfile = request.FILES['package'] category = Category.objects.all().filter(name__exact=category_name) vendor = Vendor.objects.all().filter(name__exact=vendor_name) - if len(category) == 0: + if not category: raise Exception('Non-existing category') - if len(vendor) == 0: + if not vendor: raise Exception('Non-existing vendor') try: @@ -159,7 +166,11 @@ def upload(request): myfile.seek(0) try: - savePackageFile(pkgdata, myfile, category[0], vendor[0], description, shortdescription) + package_metadata = {'category':category[0], + 'vendor':vendor[0], + 'description':description, + 'short_description':shortdescription} + savePackageFile(pkgdata, myfile, package_metadata) except Exception as error: raise Exception(error) else: @@ -174,29 +185,15 @@ def appList(request): apps = App.objects.all() dictionary = getRequestDictionary(request) if 'filter' in dictionary: - apps = apps.filter(name__contains = dictionary['filter']) + apps = apps.filter(name__contains=dictionary['filter']) if 'category_id' in dictionary: catId = dictionary['category_id'] if catId != -1: # All metacategory - apps = apps.filter(category__exact = catId) - - #Tag filtering - #"require_tag", "conflicts_tag" - # Tags are combined by logical AND (for require) and logical OR for conflicts - if 'require_tag' in request.session: - require_tags = SoftwareTagList() - require_tags.parse(request.session['require_tag']) - for i in require_tags.make_regex(): - apps = apps.filter(Q(tags__regex = i)) - if 'conflicts_tag' in request.session: - conflict_tags = SoftwareTagList() - conflict_tags.parse(request.session['conflicts_tag']) - for i in conflict_tags.make_regex(): - apps = apps.filter(~Q(tags__regex=i)) + apps = apps.filter(category__exact=catId) # Here goes the logic of listing packages when multiple architectures are available - # in /hello request, the target architecture is stored in the session. By definition target machine can support - # both "All" package architecture and it's native one. + # in /hello request, the target architecture is stored in the session. By definition + # target machine can support both "All" package architecture and it's native one. # So - here goes filtering by list of architectures archlist = ['All', ] if 'architecture' in request.session: @@ -206,52 +203,93 @@ def appList(request): if 'pkgversions' in request.session: versionlist = request.session['pkgversions'] - apps = apps.filter(architecture__in = archlist) - apps = apps.filter(pkgformat__in = versionlist) - - # After filtering, there are potential duplicates in list. And we should prefer native applications to pure qml ones - # due to speedups offered. - # So - first applications are grouped by appid and numbers of same appids counted. In case where is more than one appid - - # there are two variants of application: for 'All' architecture, and for the architecture supported by the target machine. - # So, as native apps should be preferred - duplicates = ( - apps.values('appid').order_by().annotate(count_id=Count('id')).filter(count_id__gt=1) - ) - # Here we go over duplicates list and filter out 'All' architecture apps. - for duplicate in duplicates: - apps = apps.filter(~Q(appid__exact = duplicate['appid'], architecture__exact = 'All')) # if there is native - 'All' architecture apps are excluded - - appList = list(apps.values('appid', 'name', 'vendor__name', 'briefDescription', 'category', 'tags', 'architecture', 'version').order_by('appid','architecture')) + apps = apps.filter(architecture__in=archlist) + apps = apps.filter(pkgformat__in=versionlist) - for app in appList: + #Tag filtering + #There is no search by version distance yet - this must be fixed + if 'tag' in request.session: + tags = SoftwareTagList() + tags.parse(request.session['tag']) + apps_ids = [app.id for app in apps if app.is_tagmatching(tags.list())] + apps = App.objects.filter(id__in=apps_ids) + + # After filtering, there are potential duplicates in list. And we should prefer native + # applications to pure qml ones due to speedups offered. + # At this point - filtering duplicates is disabled, because it will be implemented with + # 'distance' between requested version and package version. Architecture will be also included + # in this metric (as native apps should be preferred) + + selectedAppList = apps.values('id', 'appid', 'name', 'vendor__name', 'briefDescription', + 'category', 'architecture', + 'version', 'pkgformat', 'tags_hash').order_by('appid', 'architecture', 'tags_hash') + + appList = [] + for app in selectedAppList: + app['purchaseId'] = app['id'] app['id'] = app['appid'] app['category_id'] = app['category'] - app['category'] = Category.objects.all().filter(id__exact = app['category_id'])[0].name + app['category'] = Category.objects.all().filter(id__exact=app['category_id'])[0].name app['vendor'] = app['vendor__name'] - if app['tags'] != "": - app['tags'] = app['tags'].split(',') + app['tags'] = [] + tags = list(App.objects.filter(appid=app['appid'], architecture=app['architecture'], tags_hash=app['tags_hash'], tags__negative=False).values('tags__name', 'tags__version')) + for i in tags: + app['tags'].append(i['tags__name'] if not i['tags__version'] else ':'.join((i['tags__name'],i['tags__version']))) + app['conflict_tags'] = [] + conflict_tags = list(App.objects.filter(appid=app['appid'], architecture=app['architecture'], tags_hash=app['tags_hash'], tags__negative=True).values('tags__name', 'tags__version')) + for i in conflict_tags: + app['conflict_tags'].append(i['tags__name'] if not i['tags__version'] else ':'.join((i['tags__name'],i['tags__version']))) + + toFile = '_'.join([app['appid'], app['architecture'], app['tags_hash']]) + if settings.URL_PREFIX != '': + iconUri = '/' + settings.URL_PREFIX + '/app/icons/' + toFile else: - app['tags'] = [] + iconUri = '/app/icons/' + toFile + app['iconUrl'] = request.build_absolute_uri(iconUri) del app['vendor__name'] del app['appid'] + del app['tags_hash'] + appList.append(app) # this is not valid JSON, since we are returning a list! - return JsonResponse(appList, safe = False) + return JsonResponse(appList, safe=False) def appDescription(request): archlist = ['All', ] if 'architecture' in request.session: archlist.append(request.session['architecture']) + versionlist = [1] + if 'pkgversions' in request.session: + versionlist = request.session['pkgversions'] appId = getRequestDictionary(request)['id'] try: - app = App.objects.get(appid__exact = appId, architecture__in = archlist).order_by('architecture') + app = App.objects.filter(appid__exact = appId, architecture__in = archlist).order_by('architecture','tags_hash') + app = app.filter(pkgformat__in=versionlist) + #Tag filtering + #There is no search by version distance yet - this must be fixed + if 'tag' in request.session: + tags = SoftwareTagList() + tags.parse(request.session['tag']) + app_ids = [x.id for x in app if x.is_tagmatching(tags.list())] + app = App.objects.filter(id__in=app_ids) app = app.last() return HttpResponse(app.description) except: raise Http404('no such application: %s' % appId) +def appIconNew(request, path): + path=path.replace('/', '_').replace('\\', '_').replace(':', 'x3A').replace(',', 'x2C') + '.png' + try: + response = HttpResponse(content_type='image/png') + with open(iconPath() + path, 'rb') as pkg: + response.write(pkg.read()) + response['Content-Length'] = pkg.tell() + return response + except: + raise Http404 + def appIcon(request): archlist = ['All', ] dictionary = getRequestDictionary(request) @@ -259,12 +297,23 @@ def appIcon(request): archlist.append(normalizeArch(dictionary['architecture'])) elif 'architecture' in request.session: archlist.append(request.session['architecture']) + versionlist = [1] + if 'pkgversions' in request.session: + versionlist = request.session['pkgversions'] appId = dictionary['id'] try: - app = App.objects.filter(appid__exact = appId, architecture__in = archlist).order_by('architecture') + app = App.objects.filter(appid__exact = appId, architecture__in = archlist).order_by('architecture','tags_hash') + app = app.filter(pkgformat__in=versionlist) + #Tag filtering + #There is no search by version distance yet - this must be fixed + if 'tag' in request.session: + tags = SoftwareTagList() + tags.parse(request.session['tag']) + app_ids = [x.id for x in app if x.is_tagmatching(tags.list())] + app = App.objects.filter(id__in=app_ids) app = app.last() - with open(iconPath(app.appid,app.architecture,app.tags), 'rb') as iconPng: - response = HttpResponse(content_type = 'image/png') + with open(iconPath(app.appid, app.architecture, app.tags_hash), 'rb') as iconPng: + response = HttpResponse(content_type='image/png') response.write(iconPng.read()) return response except: @@ -277,6 +326,10 @@ def appPurchase(request): archlist = ['All', ] if 'architecture' in request.session: archlist.append(request.session['architecture']) + versionlist = [1] + if 'pkgversions' in request.session: + versionlist = request.session['pkgversions'] + try: deviceId = str(getRequestDictionary(request).get("device_id", "")) if settings.APPSTORE_BIND_TO_DEVICE_ID: @@ -285,13 +338,28 @@ def appPurchase(request): else: deviceId = '' - app = App.objects.filter(appid__exact = getRequestDictionary(request)['id'], architecture__in=archlist).order_by('architecture') + if 'id' in getRequestDictionary(request): + app = App.objects.filter(appid__exact = getRequestDictionary(request)['id'], architecture__in=archlist).order_by('architecture','tags_hash') + elif 'purchaseId' in getRequestDictionary(request): + app = App.objects.filter(id__exact = getRequestDictionary(request)['purchaseId'], architecture__in=archlist).order_by('architecture','tags_hash') + else: + raise ValidationError('id or purchaseId parameter required') + app = app.filter(pkgformat__in=versionlist) + #Tag filtering + #There is no search by version distance yet - this must be fixed + if 'tag' in request.session: + tags = SoftwareTagList() + tags.parse(request.session['tag']) + app_ids = [x.id for x in app if x.is_tagmatching(tags.list())] + app = App.objects.filter(id__in=app_ids) + app = app.last() - fromFilePath = packagePath(app.appid, app.architecture, app.tags) + fromFilePath = packagePath(app.appid, app.architecture, app.tags_hash) # we should not use obvious names here, but just hash the string. # this would be a nightmare to debug though and this is a development server :) - toFile = str(app.appid) + '_' + str(request.user.id) + '_' + str(app.architecture) + '_' + str(app.tags) + '_'+ str(deviceId) + toFile = '_'.join((str(app.appid), str(request.user.id), str(app.architecture), + str(app.tags_hash), str(deviceId))) if not settings.DEBUG: toFile = hashlib.sha256(toFile).hexdigest() toFile += '.appkg' @@ -309,7 +377,7 @@ def appPurchase(request): shutil.copyfile(fromFilePath, toPath + toFile) except Exception as error: if type(error) == IOError: - raise IOError(error.args[0],error.args[1], os.path.basename(fromFilePath)) + raise IOError(error.args[0], error.args[1], os.path.basename(fromFilePath)) else: raise error @@ -330,7 +398,7 @@ def appPurchase(request): def appDownload(request, path): try: - response = HttpResponse(content_type = 'application/octetstream') + response = HttpResponse(content_type='application/octetstream') with open(downloadPath() + path, 'rb') as pkg: response.write(pkg.read()) response['Content-Length'] = pkg.tell() diff --git a/store/management/commands/store-upload-package.py b/store/management/commands/store-upload-package.py index 77286bc..7af866b 100644 --- a/store/management/commands/store-upload-package.py +++ b/store/management/commands/store-upload-package.py @@ -30,18 +30,22 @@ ## ############################################################################# -import os - +from optparse import make_option from django.core.management.base import BaseCommand, CommandError from django.core.files.base import ContentFile -from store.models import App, Category, Vendor, savePackageFile +from store.models import Category, Vendor, savePackageFile from store.utilities import parseAndValidatePackageMetadata -from optparse import make_option + class Command(BaseCommand): help = 'Uploads a package to the deployment server. This can be used for batch uploading.' - option_list = BaseCommand.option_list + ( + usage_string = 'Usage: manage.py store-upload-package --vendor <vendor> --category <category>' \ + ' [--description <short description>] <package>' + + # FIXME: this doesn't work: + # see https://docs.djangoproject.com/en/1.8/howto/custom-management-commands/#django.core.management.BaseCommand.add_arguments + self.option_list = BaseCommand.option_list + ( make_option('--vendor', action='store', type="string", @@ -62,16 +66,14 @@ class Command(BaseCommand): def handle(self, *args, **options): if len(args) != 1: - raise CommandError( - 'Usage: manage.py store-upload-package --vendor <vendor> --category <category> [--description <short description>] <package>') + raise CommandError(self.usage_string) if (not options['vendor']) or (not options['category']): - raise CommandError( - 'Usage: manage.py store-upload-package --vendor <vendor> --category <category> [--description <short description>] <package>') + raise CommandError(self.usage_string) category = Category.objects.all().filter(name__exact=options['category']) vendor = Vendor.objects.all().filter(name__exact=options['vendor']) - if len(category) == 0: + if not category: raise CommandError('Non-existing category specified') - if len(vendor) == 0: + if not vendor: raise CommandError('Non-existing vendor specified') try: @@ -86,7 +88,10 @@ class Command(BaseCommand): packagefile.seek(0) description = options['description'] try: - savePackageFile(pkgdata, ContentFile(packagefile.read()), category[0], vendor[0], description, description) + package_metadata = {'category': category[0], + 'vendor': vendor[0], + 'description': description, + 'short_description': description} + savePackageFile(pkgdata, ContentFile(packagefile.read()), package_metadata) except Exception as error: raise CommandError(error) - diff --git a/store/migrations/0001_initial.py b/store/migrations/0001_initial.py index f23c6bf..80d154c 100644 --- a/store/migrations/0001_initial.py +++ b/store/migrations/0001_initial.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# Generated by Django 1.11.27 on 2020-08-14 17:24 ############################################################################# ## ## Copyright (C) 2020 Luxoft Sweden AB @@ -38,6 +39,7 @@ from django.conf import settings from django.db import migrations, models import django.db.models.deletion import store.models +import uuid class Migration(migrations.Migration): @@ -52,7 +54,7 @@ class Migration(migrations.Migration): migrations.CreateModel( name='App', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('appid', models.CharField(max_length=200)), ('name', models.CharField(max_length=200)), ('file', models.FileField(storage=store.models.OverwriteStorage(), upload_to=store.models.content_file_name)), @@ -60,7 +62,7 @@ class Migration(migrations.Migration): ('description', models.TextField()), ('dateAdded', models.DateField(auto_now_add=True)), ('dateModified', models.DateField(auto_now=True)), - ('tags', models.TextField(blank=True)), + ('tags_hash', models.CharField(default=b'', max_length=4096)), ('architecture', models.CharField(default=b'All', max_length=20)), ('version', models.CharField(default=b'0.0.0', max_length=20)), ('pkgformat', models.IntegerField()), @@ -80,6 +82,15 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('negative', models.BooleanField(default=False)), + ('name', models.CharField(max_length=200)), + ('version', models.CharField(blank=True, max_length=200)), + ], + ), + migrations.CreateModel( name='Vendor', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), @@ -88,6 +99,10 @@ class Migration(migrations.Migration): ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], ), + migrations.AlterUniqueTogether( + name='tag', + unique_together=set([('negative', 'name', 'version')]), + ), migrations.AddField( model_name='app', name='category', @@ -95,11 +110,16 @@ class Migration(migrations.Migration): ), migrations.AddField( model_name='app', + name='tags', + field=models.ManyToManyField(to='store.Tag'), + ), + migrations.AddField( + model_name='app', name='vendor', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='store.Vendor'), ), migrations.AlterUniqueTogether( name='app', - unique_together=set([('appid', 'architecture', 'tags')]), + unique_together=set([('appid', 'architecture', 'tags_hash')]), ), ] diff --git a/store/models.py b/store/models.py index 1cf8916..7e8751a 100644 --- a/store/models.py +++ b/store/models.py @@ -31,14 +31,16 @@ ############################################################################# import os +import uuid from django.db import models -from ordered_model.models import OrderedModel from django.conf import settings from django.contrib.auth.models import User from django.core.files.storage import FileSystemStorage +from ordered_model.models import OrderedModel -from utilities import packagePath, writeTempIcon, makeTagList +from store.utilities import packagePath, writeTempIcon, makeTagList +from store.tags import SoftwareTag def category_file_name(instance, filename): # filename parameter is unused. See django documentation for details: @@ -66,8 +68,8 @@ class Category(OrderedModel): # This is a django hack. When category icon is saved and then later accessed, # category_id is used as a unique icon identifier. When category is first created, # but not saved yet, category_id is None. So this hack first saves category without icon - # and then saves the icon separately. This is done to prevent creation of category_None.png - # file, when the icon is saved. + # and then saves the icon separately. This is done to prevent creation of + # category_None.png file, when the icon is saved. saved_icon = self.icon self.icon = None super(Category, self).save(*args, **kwargs) @@ -82,76 +84,133 @@ class Vendor(models.Model): def __unicode__(self): return self.name +class Tag(models.Model): + negative = models.BooleanField(default=False) + name = models.CharField(max_length=200) + version = models.CharField(max_length=200, blank=True) + + class Meta: + unique_together = (('negative', 'name', 'version')) + + def __unicode__(self): + negative = '-' if self.negative else '' + if self.version: + return negative + self.name + ":" + self.version + return negative + self.name def content_file_name(instance, filename): - return packagePath(instance.appid, instance.architecture, instance.tags) + return packagePath(instance.appid, instance.architecture, instance.tags_hash) class App(models.Model): - appid = models.CharField(max_length = 200) - name = models.CharField(max_length = 200) - file = models.FileField(upload_to = content_file_name, storage = OverwriteStorage()) + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + appid = models.CharField(max_length=200) + name = models.CharField(max_length=200) + file = models.FileField(upload_to=content_file_name, storage=OverwriteStorage()) vendor = models.ForeignKey(Vendor) category = models.ForeignKey(Category) briefDescription = models.TextField() description = models.TextField() - dateAdded = models.DateField(auto_now_add = True) - dateModified = models.DateField(auto_now = True) - tags = models.TextField(blank=True) + dateAdded = models.DateField(auto_now_add=True) + dateModified = models.DateField(auto_now=True) + tags = models.ManyToManyField(Tag) + # Hash sum for sorted tag list, this is used to be able to distinguish different tag sets + tags_hash = models.CharField(max_length=4096, default='') architecture = models.CharField(max_length=20, default='All') version = models.CharField(max_length=20, default='0.0.0') pkgformat = models.IntegerField() class Meta: """Makes the group of id and arch - a unique identifier""" - unique_together = (('appid', 'architecture', 'tags'),) + unique_together = (('appid', 'architecture', 'tags_hash'),) def __unicode__(self): - return self.name + " [" + " ".join([self.appid, self.version, self.architecture, self.tags]) + "]" + return self.name + " [" + " ".join([self.appid, self.version, self.architecture, + self.tags_hash]) + "]" + + def is_tagmatching(self, tagstring): + temp = self.tags.all() + if not temp: #App with tags does not require anything + return True + for i in temp: + if not any(j.match(SoftwareTag(str(i))) for j in tagstring): + return False + return True def save(self, *args, **kwargs): try: - this = App.objects.get(appid=self.appid,architecture=self.architecture,tags=self.tags) #FIXME: This should be 'tags match, not exact same tags' + this = App.objects.get(appid=self.appid, architecture=self.architecture, tags_hash=self.tags_hash) if this.file != self.file: this.file.delete(save=False) except: pass super(App, self).save(*args, **kwargs) - -def savePackageFile(pkgdata, pkgfile, category, vendor, description, shortdescription): +def populateTagList(tags, conflict_tags): + taglist = [] + for i in tags: + # Add tag to database + version = "" if i.version is None else i.version + tag, created = Tag.objects.get_or_create(name=i.tag, version=version, negative=False) + if created: + tag.save() + taglist.append(tag) + for i in conflict_tags: + # Add tag to database + version = "" if i.version is None else i.version + tag, created = Tag.objects.get_or_create(name=i.tag, version=version, negative=True) + if created: + tag.save() + return taglist + + +def savePackageFile(pkgdata, pkgfile, package_metadata): appId = pkgdata['info']['id'] name = pkgdata['storeName'] architecture = pkgdata['architecture'] pkgformat = pkgdata['packageFormat']['formatVersion'] - tags = makeTagList(pkgdata) - success, error = writeTempIcon(appId, architecture, tags, pkgdata['icon']) + tags, conflict_tags, tags_hash = makeTagList(pkgdata) + category = package_metadata['category'] + vendor = package_metadata['vendor'] + description = package_metadata['description'] + shortdescription = package_metadata['short_description'] + + success, error = writeTempIcon(appId, architecture, tags_hash, pkgdata['icon']) if not success: raise Exception(error) exists = False app = None try: - app = App.objects.get(appid__exact=appId, architecture__exact=architecture, tags__exact=tags) + app = App.objects.get(appid__exact=appId, architecture__exact=architecture, + tags_hash__exact=tags_hash) exists = True except App.DoesNotExist: pass + taglist = populateTagList(tags.list(), conflict_tags.list()) + if exists: app.appid = appId app.category = category app.vendor = vendor app.name = name - app.tags = tags + app.tags_hash = tags_hash app.description = description app.briefDescription = shortdescription app.architecture = architecture app.pkgformat = pkgformat - app.file.save(packagePath(appId, architecture, tags), pkgfile) + app.file.save(packagePath(appId, architecture, tags_hash), pkgfile) app.save() else: - app, _ = App.objects.get_or_create(name=name, tags=tags, vendor=vendor, - category=category, appid=appId, - briefDescription=shortdescription, description=description, - pkgformat = pkgformat, architecture=architecture) - app.file.save(packagePath(appId, architecture, tags), pkgfile) + app, _ = App.objects.get_or_create(name=name, tags_hash=tags_hash, + vendor=vendor, category=category, appid=appId, + briefDescription=shortdescription, + description=description, pkgformat=pkgformat, + architecture=architecture) #FIXME + app.file.save(packagePath(appId, architecture, tags_hash), pkgfile) app.save() + + for i in taglist: + # attach tags to app + app.tags.add(i) + diff --git a/store/osandarch.py b/store/osandarch.py index 7b833d3..5668b48 100644 --- a/store/osandarch.py +++ b/store/osandarch.py @@ -32,12 +32,14 @@ # check for file type here. # those are expected types -# ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.18, not stripped +# ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, +# interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.18, not stripped # ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, not stripped # ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, not stripped # Mach-O 64-bit x86_64 dynamically linked shared library # Mach-O 64-bit x86_64 executable -# Mach-O universal binary with 2 architectures: [x86_64: Mach-O 64-bit x86_64 bundle] [i386: Mach-O i386 bundle] [] +# Mach-O universal binary with 2 architectures: +# [x86_64: Mach-O 64-bit x86_64 bundle] [i386: Mach-O i386 bundle] [] # PE32+ executable (console) x86-64, for MS Windows # PE32+ executable (DLL) (console) x86-64, for MS Windows # PE32+ executable (DLL) (GUI) x86-64, for MS Windows @@ -45,21 +47,21 @@ import re -def parseMachO(str): # os, arch, bits, endianness - if " universal " in str: +def parseMachO(string_data): # os, arch, bits, endianness + if " universal " in string_data: # Universal binary - not supported raise Exception("Universal binaries are not supported in packages") os = "macOS" - arch = str.split(' ') + arch = string_data.split(' ') arch = arch[2] - bits = str.split(' ')[1].replace('-bit', '') + bits = string_data.split(' ')[1].replace('-bit', '') endianness = "little_endian" return [os, arch, bits, endianness] -def parsePE32(str): +def parsePE32(string_data): os = "Windows" - arch = str.split(',') + arch = string_data.split(',') arch = arch[0] # Take first part arch = arch.split(' ') arch = arch[-1] # Take last element @@ -72,12 +74,13 @@ def parsePE32(str): return [os, arch, bits, endianness] -def parseElfArch(str, architecture, bits): +def parseElfArch(string_data, architecture, bits): architecture = architecture.strip() if architecture.startswith("ARM"): if 'aarch64' in architecture: return 'arm64' - if 'armhf' in str: # this does not work for some reason - from_file() returns longer data than from_buffer() - needs fix + if 'armhf' in string_data: # this does not work for some reason - from_file() returns + # longer data than from_buffer() - needs fix return 'arm' # because qt does not report it directly elif architecture.startswith("Intel"): if '80386' in architecture: @@ -93,13 +96,13 @@ def parseElfArch(str, architecture, bits): return architecture.lower() -def parseElf(str): +def parseElf(string_data): os = "Linux" - arch = str.split(',') + arch = string_data.split(',') arch = arch[1] - bits = str.split(' ')[1].replace('-bit', '') - arch = parseElfArch(str, arch, bits) - endian = str.split(' ')[2].lower() + bits = string_data.split(' ')[1].replace('-bit', '') + arch = parseElfArch(string_data, arch, bits) + endian = string_data.split(' ')[2].lower() if endian == "msb": endianness = "big_endian" elif endian == "lsb": @@ -109,21 +112,21 @@ def parseElf(str): return [os, arch, bits, endianness] -def getOsArch(str): +def getOsArch(string_data): os = None arch = None bits = None endianness = None fmt = None - if str.startswith("ELF "): + if string_data.startswith("ELF "): fmt = "elf" - os, arch, bits, endianness = parseElf(str) - elif str.startswith("Mach-O "): + os, arch, bits, endianness = parseElf(string_data) + elif string_data.startswith("Mach-O "): fmt = "mach_o" - os, arch, bits, endianness = parseMachO(str) - elif str.startswith("PE32+ ") or str.startswith("PE32 "): + os, arch, bits, endianness = parseMachO(string_data) + elif string_data.startswith("PE32+ ") or string_data.startswith("PE32 "): fmt = "pe32" - os, arch, bits, endianness = parsePE32(str) + os, arch, bits, endianness = parsePE32(string_data) if arch: arch = arch.replace('-', '_') result = [os, arch, endianness, bits, fmt] @@ -133,9 +136,10 @@ def getOsArch(str): def normalizeArch(inputArch): """ - This function brings requested architecture to common form (currently just parses the bits part and turns it into 32/64) - Input string format is: arch-endianness-word_size-optional_ABI-kernelType - Output string format is: arch-endianness-word_size-kernelType + This function brings requested architecture to common form (currently just parses the bits + part and turns it into 32/64) + Input string format is: arch-endianness-word_size-optional_ABI-kernelType + Output string format is: arch-endianness-word_size-kernelType """ parts = inputArch.split('-') diff --git a/store/tags.py b/store/tags.py index aa73006..e906684 100644 --- a/store/tags.py +++ b/store/tags.py @@ -37,6 +37,10 @@ import re def validateTagVersion(version): + """ + Validates tag version, in string form + :type tag: str + """ for i in version: if not i.isalnum(): if not ((i == "_") or (i == ".")): @@ -45,7 +49,11 @@ def validateTagVersion(version): def validateTag(tag): - if len(tag) == 0: + """ + Validates tag (with version), in string form + :type tag: str + """ + if not tag: return False lst = tag.split(':') if len(lst) > 2: @@ -60,14 +68,21 @@ def validateTag(tag): class SoftwareTag: + """ + This class represents one tag instance. Tag is formatted in string form as two parts: + * tag itself, which is supposed to contain only alphanumeric symbols and _ + * optional tag version, which consists of any non-zero number of alphanumeric parts, + separated by dots. +""" + def __init__(self, tag): """ Takes tag and parses it. If it can't parse - raises exception of invalid value :type tag: str """ - if not ((type(tag) == str) or (type(tag) == unicode)): - raise (BaseException("Invalid input data-type")) + if not isinstance(tag, (str, unicode)): + raise BaseException("Invalid input data-type") if not validateTag(tag): - raise (BaseException("Malformed tag")) + raise BaseException("Malformed tag") tag_version = tag.split(':') self.tag = tag_version[0].lower() # No, this should be lowercase self.version = None if len(tag_version) == 1 else tag_version[1] @@ -78,37 +93,27 @@ class SoftwareTag: def __str__(self): if self.version: return "%s:%s" % (self.tag, self.version) - else: - return self.tag + return self.tag def has_version(self): return self.version is not None def match(self, tag): # self is "on server", tag is "request" + """ + Does tag matching for tag string/tag list minimisation + :param tag: tags.SoftwareTag + :return: Returns true, if self is more specific (or equal) than tag + """ if self.tag == tag.tag: # Names are the same, that is good, matching versions now. if self.version == tag.version: return True - else: - if tag.version is None: - return True # qt in request, anything else on server - True - if self.version is not None and self.version.startswith(tag.version + "."): - return True - return False + if tag.version is None: + return True + if self.version is not None and self.version.startswith(tag.version + "."): + return True return False - def make_regex(self): - if self.version is None: - # versionless tag - temp_string = re.escape(self.tag) - regex = "(%s:[a-z0-9_.]*)|(%s)" % (temp_string, temp_string,) - else: - # tag with versions - temp_string = re.escape("%s:%s" % (self.tag, self.version)) - regex = "(%s\.[a-z0-9_.]*)|(%s)" % (temp_string, temp_string) - return regex - - class SoftwareTagList: def __init__(self): # dictionary of tags, key is - tag name @@ -116,7 +121,7 @@ class SoftwareTagList: def __str__(self): lst = list() - for key, value in self.taglist.items(): + for _, value in self.taglist.items(): lst += [str(i) for i in value] lst.sort() return ",".join(lst) @@ -136,7 +141,8 @@ class SoftwareTagList: def has_version(self, tag_name): if tag_name in self.taglist: - # This check is possible, because, when there is tag without version - it is the only tag in the list + # This check is possible, because, when there is tag without version - + # it is the only tag in the list if self.taglist[tag_name][0].has_version(): return True return False @@ -160,41 +166,13 @@ class SoftwareTagList: def is_empty(self): return len(self.taglist) == 0 - def make_regex(self): + def list(self): lst = list() - for key, value in self.taglist.items(): - regex = "(^|,)%s(,|$)" % "|".join([i.make_regex() for i in value]) - lst.append(regex) + for _, value in self.taglist.items(): + for i in value: + lst.append(i) return lst - def match_positive(self, taglist): - # checks that everything from tag list matches current tags - # Start checking with checking if all requested tags in taglist are present in self.taglist - for i in taglist.taglist: - if i not in self.taglist: - return False - # Now we need to check if versions are matching - for tag in taglist.taglist: - if not self.has_version(tag): - # If package tag accepts anything - it already matches, next please - continue - if taglist.has_version(tag) and not any(v1.match(v) for v in taglist[tag] for v1 in self[tag]): - return False - return True - - def match_negative(self, taglist): - # checks that nothing from taglist matches current tags - for i in taglist.taglist: - if i in self.taglist: - if (not taglist.has_version(i)) or (not self.has_version(i)): - return False - # Tag found, version list is present. check if it matches, if it does - check is failed - for version in taglist[i]: - for version1 in self[i]: - if version1.match(version): - return False - return True - def hash(self): # Looks like the list is sorted, but well... return hashlib.md5(str(self)).hexdigest() @@ -247,43 +225,6 @@ class TestSoftwareTagListMethods(unittest.TestCase): lst.append(SoftwareTag('qt')) self.assertFalse(lst.is_empty()) - def test_empty_matches_everything(self): - empty_list = SoftwareTagList() - test_list = SoftwareTagList() - test_list.append(SoftwareTag('qt')) - self.assertTrue(test_list.match_positive(empty_list)) - self.assertTrue(test_list.match_negative(empty_list)) - - def test_match_positive(self): - list_to_test = SoftwareTagList() - list_to_test.parse("qt:5.1,neptune,test:1,second_test") - matching_list = SoftwareTagList() - matching_list.parse("qt") - self.assertTrue(list_to_test.match_positive(matching_list)) - matching_list.parse("qt:5.1") - self.assertTrue(list_to_test.match_positive(matching_list)) - matching_list.parse("qt:5.1,qt:5.2,neptune:1") - self.assertTrue(list_to_test.match_positive(matching_list)) - matching_list.parse("qt:5.1,test:2") - self.assertFalse(list_to_test.match_positive(matching_list)) - matching_list.parse("qt:5.1.1") - self.assertFalse(list_to_test.match_positive(matching_list)) - - def test_match_negative(self): - list_to_test = SoftwareTagList() - list_to_test.parse("qt:5.1,neptune") - matching_list = SoftwareTagList() - matching_list.parse("qt") - self.assertFalse(list_to_test.match_negative(matching_list)) - matching_list.parse("qt:5.1") - self.assertFalse(list_to_test.match_negative(matching_list)) - matching_list.parse("qt:5.1,qt:5.2,neptune:1") - self.assertFalse(list_to_test.match_negative(matching_list)) - matching_list.parse("qt:5.1,qt:5.2") - self.assertFalse(list_to_test.match_negative(matching_list)) - matching_list.parse("test") - self.assertTrue(list_to_test.match_negative(matching_list)) - def test_append_invalid(self): lst = SoftwareTagList() with self.assertRaisesRegexp(BaseException, "Malformed tag"): diff --git a/store/utilities.py b/store/utilities.py index 1205f1b..74ffc98 100644 --- a/store/utilities.py +++ b/store/utilities.py @@ -43,38 +43,48 @@ from django.conf import settings from OpenSSL.crypto import load_pkcs12, FILETYPE_PEM, dump_privatekey, dump_certificate from M2Crypto import SMIME, BIO, X509 -from tags import SoftwareTagList, SoftwareTag -import osandarch +from store.tags import SoftwareTagList, SoftwareTag +import store.osandarch def makeTagList(pkgdata): + """Generates tag lists out of package data + First list - required tags, second list - conflicting tags + """ taglist = SoftwareTagList() + tagconflicts = SoftwareTagList() for fields in ('extra', 'extraSigned'): if fields in pkgdata['header']: if 'tags' in pkgdata['header'][fields]: for i in list(pkgdata['header'][fields]['tags']): # Fill tags list then add them taglist.append(SoftwareTag(i)) - return str(taglist) + if 'conflicts' in pkgdata['header'][fields]: + for i in list(pkgdata['header'][fields]['conflicts']): + tagconflicts.append(SoftwareTag(i)) + + tags_hash = str(taglist) + str(tagconflicts) + return taglist, tagconflicts, tags_hash def getRequestDictionary(request): if request.method == "POST": return request.POST - else: - return request.GET + return request.GET -def packagePath(appId = None, architecture = None, tags = None): +def packagePath(appId=None, architecture=None, tags=None): path = settings.MEDIA_ROOT + 'packages/' if tags is None: tags = "" if (appId is not None) and (architecture is not None): - path = path + '_'.join([appId, architecture, tags]).replace('/','_').replace('\\','_').replace(':','x3A').replace(',','x2C') + path = path + '_'.join([appId, architecture, tags]).replace('/', '_').\ + replace('\\', '_').replace(':', 'x3A').replace(',', 'x2C') return path -def iconPath(appId = None, architecture = None, tags = None): +def iconPath(appId=None, architecture=None, tags=None): path = settings.MEDIA_ROOT + 'icons/' if tags is None: tags = "" if (appId is not None) and (architecture is not None): - return path + '_'.join([appId, architecture, tags]).replace('/','_').replace('\\','_').replace(':','x3A').replace(',','x2C') + '.png' + return path + '_'.join([appId, architecture, tags]).replace('/', '_').\ + replace('\\', '_').replace(':', 'x3A').replace(',', 'x2C') + '.png' return path def writeTempIcon(appId, architecture, tags, icon): @@ -87,7 +97,8 @@ def writeTempIcon(appId, architecture, tags, icon): tempicon.close() return True, None except IOError as error: - return False, 'Validation error: could not write icon file to media directory: %s' % str(error) + return False, 'Validation error: could not write icon file to media directory: %s' % \ + str(error) def downloadPath(): return settings.MEDIA_ROOT + 'downloads/' @@ -300,7 +311,7 @@ def parsePackageMetadata(packageFile): fil.seek(0) #from_buffer instead of from_file filemagic = ms.from_file(fil.name) fil.close() - osarch = osandarch.getOsArch(filemagic) + osarch = store.osandarch.getOsArch(filemagic) if osarch: #[os, arch, endianness, bits, fmt] architecture = '-'.join(osarch[1:]) osset.add(osarch[0]) @@ -416,7 +427,7 @@ def parseAndValidatePackageMetadata(packageFile, certificates = []): certificates = [] for certFile in settings.APPSTORE_DEV_VERIFY_CA_CERTIFICATES: with open(certFile, 'rb') as cert: - certificates.append(cert.read()) + certificates.append(cert.read()) verifySignature(pkgdata['footer']['developerSignature'], pkgdata['rawDigest'], certificates) |