summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNikolay Zamotaev <nzamotaev@luxoft.com>2020-02-20 17:13:17 +0300
committerNikolay Zamotaev <nzamotaev@luxoft.com>2020-08-18 15:34:02 +0000
commit23450bceb6b9d032b7444ab25947d405dcb441d8 (patch)
treea297c24bb6cd90ce80903c64a21b1d5f230756fe
parent4f9f1fc171601d151ee0208efbfd0ffa9e73b501 (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.py4
-rw-r--r--appstore/urls.py7
-rw-r--r--appstore/wsgi.py3
-rw-r--r--doc/src/deployment-server-reference.qdoc28
-rw-r--r--store/admin.py59
-rw-r--r--store/api.py204
-rw-r--r--store/management/commands/store-upload-package.py31
-rw-r--r--store/migrations/0001_initial.py26
-rw-r--r--store/models.py111
-rw-r--r--store/osandarch.py54
-rw-r--r--store/tags.py131
-rw-r--r--store/utilities.py35
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)