source: lliurex-store/trunk/fuentes/python3-lliurex-store.install/usr/share/lliurexstore/plugins/loadStore.py @ 5214

Last change on this file since 5214 was 5214, checked in by Juanma, 22 months ago

added bs4 to loadstore

File size: 13.2 KB
Line 
1import os
2import gi
3from gi.repository import Gio
4gi.require_version('AppStreamGlib', '1.0')
5from gi.repository import AppStreamGlib as appstream
6import subprocess
7import json
8import re
9import urllib
10import random
11import threading
12import time
13import datetime
14import gettext
15from bs4 import BeautifulSoup
16gettext.textdomain('python3-lliurex-store')
17_=gettext.gettext
18
19class loadstore:
20        def __init__(self):
21                self.dbg=0
22                self.pluginInfo={'load':'*'}
23                self.store=''
24                self.progress=0
25                self.error=0
26                self.bundleDir="/var/lib/lliurexstore/bundles"
27                self.bundleTypes=['appimg']
28                self.zmdCatalogueDir='/var/lib/lliurexstore/zmds'
29        #def __init__
30
31        def set_debug(self,dbg='1'):
32                self.dbg=int(dbg)
33                self._debug ("Debug enabled")
34        #def set_debug
35
36        def _debug(self,msg=''):
37                if self.dbg==1:
38                        print ('DEBUG Load: '+str(msg))
39        #def _debug
40
41        def register(self):
42                return(self.pluginInfo)
43        #def register
44
45        def execute_action(self,action,store=None,loadBundles=False):
46                self.progress=0
47                if store:
48                        self.store=store
49                else:
50                        self.store=appstream.Store()
51                if action=='load':
52                        self._load_store(self.store,loadBundles)
53#               self.progress=100
54                return (self.store)
55        #def execute_action
56
57        def get_error(self):
58                return (self.error)
59        #def get_error
60
61        def _load_store(self,store,loadBundles=False):
62                iconPath='/usr/share/icons/hicolor/128x128'
63                flags=[appstream.StoreLoadFlags.APP_INFO_SYSTEM,appstream.StoreLoadFlags.APP_INSTALL,appstream.StoreLoadFlags.APP_INFO_USER,appstream.StoreLoadFlags.DESKTOP,appstream.StoreLoadFlags.APPDATA,appstream.StoreLoadFlags.ALLOW_VETO]
64                for flag in flags:
65                        try:
66                                self._debug("Loading "+str(flag))
67                                store.load(flag)
68                        except:
69                                print ("Failed to load"+str(flag))
70                                pass
71                store=self.load_zmds_catalog(store)
72                store=self._sanitize_store(store)
73                if loadBundles:
74                        if self._download_bundles_catalogue():
75                                store=self.load_bundles_catalog(store)
76                self.store=store
77                return(store)
78        #def load_store
79
80        def load_bundles_catalog(self,store):
81                if os.path.exists(self.bundleDir):
82                        for bundleType in self.bundleTypes:
83                                store=self._generic_file_load(self.bundleDir+'/'+bundleType,store)
84                return(store)
85        #def load_bundles_catalog(self)
86
87        def load_zmds_catalog(self,store):
88                if os.path.exists(self.zmdCatalogueDir):
89                        store=self._generic_file_load(self.zmdCatalogueDir,store)
90                return(store)
91        #def load_zmds_catalog(self)
92
93        def _generic_file_load(self,filesDir,store):
94                iconPath='/usr/share/icons/hicolor/128x128'
95                for appFile in os.listdir(filesDir):
96                        if appFile.endswith('appdata.xml'):
97                                storePath=Gio.File.new_for_path(filesDir+'/'+appFile)
98                                self._debug("Adding file "+filesDir+'/'+appFile)
99                                try:
100                                        store.from_file(storePath,iconPath,None)
101                                except Exception as e:
102                                        self._debug("Couldn't add file "+appFile+" to store")
103                                        self._debug("Reason: "+str(e))
104                return(store)
105       
106        def _parse_desktop(self,store): #DEPRECATED. Loads the apps from the available desktop files
107                desktopDir='/usr/share/applications'
108                applist=[]
109                for desktopFile in os.listdir(desktopDir):
110                        if desktopFile.endswith('desktop'):
111                                a=appstream.App()
112                                try:
113                                        a.parse_file(desktopDir+'/'+desktopFile,16)
114                                        a.set_priority(0)
115                                        for veto in a.get_vetos():
116                                                a.remove_veto(veto)
117                                        store.add_app(a)
118                                        self._debug("Adding app from desktop "+desktopFile)
119                                except:
120                                        pass
121                return(store)
122        #def _parse_desktop
123
124        def _sanitize_store(self,store):
125                applist=store.get_apps()
126                uniqDict={}
127                lliurexApps={}
128                zmdList=[]
129                for app in applist:
130                        #Zomandos get max priority
131                        if app.has_category('Zomando'):
132                                self._debug("Prioritize zmd "+str(app.get_id()))
133                                app.set_priority(400)
134                                lliurexApps.update({app.get_id_filename():app})
135                                id=str(app.get_id_filename()).replace('zero-lliurex-','')
136                                zmdList.append(id)
137                        #Prioritize Lliurex apps
138                        elif app.has_category('Lliurex'):
139                                self._debug("Prioritize app "+str(app.get_id()))
140                                app.set_priority(200)
141                                lliurexApps.update({app.get_id_filename():app})
142                        elif str(app.get_origin()).find('lliurex')>=0:
143                                self._debug("Prioritize app "+app.get_id())
144                                app.set_priority(100)
145                                lliurexApps.update({app.get_id_filename():app})
146                        else:
147                                app.set_priority(0)
148                                if app.get_id_filename() in lliurexApps.keys():
149#                                       app.set_merge_kind(appstream.AppMergeKind.APPEND)
150                                        self._debug("Mergin app "+str(app.get_id())+" as is in Lliurex")
151#                                       app.set_id(lliurexApps[app.get_id_filename()])
152                                        lliurexApps[app.get_id_filename()].subsume_full(app,appstream.AppSubsumeFlags.BOTH_WAYS)
153                                        store.remove_app(app)
154                        #Remove apps whitout pkgname
155                        if not app.get_pkgnames():
156                                store.remove_app(app)
157                        #Remove add-on apps (as are included in the main packages)
158                        if app.get_kind()==appstream.AppKind.ADDON:
159                                self._debug("Removed addon "+str(app.get_pkgnames()))
160                                store.remove_app(app)
161                        #Remove duplicated apps
162                        #Unlike gnome-store we'll try to compare the info of the package in order of discard only the "part-of" packages
163                        pkg=app.get_pkgname_default()
164                        if pkg in uniqDict.keys():
165                                fn=app.get_id_no_prefix()
166                                self._debug("Comparing "+fn+" with "+uniqDict[pkg]['fn'])
167                                if fn != uniqDict[pkg]['fn']:
168                                        if fn != pkg:
169                                                self._debug("Removed duplicated "+app.get_id())
170                                                store.remove_app(app)
171                                        else:
172                                                self._debug("Removed duplicated "+uniqDict[pkg]['app'].get_id())
173                                                uniqDict.update({pkg:{'fn':app.get_id_no_prefix(),'app':app}})
174                                                store.remove_app(uniqDict[pkg]['app'])
175                        elif pkg:
176#                               self._debug("Adding "+app.get_id_filename()+" to uniq dict")
177                                uniqDict.update({pkg:{'fn':app.get_id_filename(),'app':app}})
178                #Delete zomando-related debs
179                store=self._purge_zomandos(zmdList,store)
180                #Check the blacklist
181                store=self._apply_blacklist(store)
182                return (store)
183        #def _sanitize_store
184
185        def _purge_zomandos(self,zmdList,store):
186                for appId in zmdList:
187                        self._debug("Searching debs related to "+appId)
188                        purgeList=store.get_apps_by_id(appId)
189                        purgeList.extend(store.get_apps_by_id(appId+".desktop"))
190                        for purgeApp in purgeList:
191                                if purgeApp:
192                                        if not purgeApp.has_category('Zomando'):
193                                                self._debug("Removed related zomando app "+str(purgeApp.get_id()))
194                                                store.remove_app(purgeApp)
195                return(store)
196        #def _purge_zomandos
197
198        def _apply_blacklist(self,store):
199                try:
200                        flavour=subprocess.check_output(["lliurex-version","-f"]).rstrip()
201                        flavour=flavour.decode("utf-8")
202                        if flavour=='None':
203                                self._debug("Unknown flavour. Switching to desktop")
204                                flavour='desktop'
205                except (subprocess.CalledProcessError,FileNotFoundError) as e:
206                                self._debug("Running on a non Lliurex host")
207                                flavour='desktop'
208                try:
209                        if os.path.isfile('/usr/share/lliurex-store/files/blacklist.json'):
210                                blFile=open('/usr/share/lliurex-store/files/blacklist.json').read()
211                                blacklist=json.loads(blFile)
212                                blApps=[]
213                                if flavour in blacklist:
214                                        blApps=blacklist[flavour]
215                                if "all" in blacklist:
216                                        blApps.extend(blacklist["all"])
217                                blRegEx=[]
218                                for blApp in blApps:
219                                        self._debug("Blacklisted app: "+blApp)
220                                        resultRe=re.search('([^a-zA-Z0-9_-])',blApp)
221#                                       if blApp[-1]!='*':
222                                        if resultRe:
223                                                if blApp[0]=='*':
224                                                        blApp='.'+blApp
225                                                blRegEx.append("("+blApp+")")
226                                        else:
227                                                app=store.get_app_by_pkgname(blApp)
228                                                if app:
229                                                        self._debug("Removed "+str(app))
230                                                        store.remove_app(app)
231                                                else:
232                                                        self._debug("App "+blApp+" from blacklist not found in store. Assigned to RE blacklist")
233                                                        blRegEx.append("("+blApp+")")
234                                if blRegEx:
235                                        self._debug("Attempting to remove apps by RE match")
236                                        for app in store.get_apps():
237                                                for blApp in blRegEx:
238                                                        resultRe=re.search(blApp,app.get_id())
239                                                        if resultRe:
240#                                                       if blApp.lower() in app.get_id_filename().lower():
241                                                                store.remove_app(app)
242                                                                self._debug("Removed "+str(app.get_id()) +" as matches with "+blApp)
243                        else:
244                                self._debug('No blacklist to check')
245                except Exception as e:
246                        self._debug("Error processing blacklist: "+str(e))
247                finally:
248                        return(store)
249        #def _apply_blacklist
250
251        def _download_bundles_catalogue(self):
252                CURSOR_UP='\033[F'
253                ERASE_LINE='\033[K'
254                content=''
255                applist=[]
256                progressBar="#"
257                repoList={'appimg':['https://dl.bintray.com/probono/AppImages']}
258                #For get the description of an app we must go to a specific url.
259                #$(appname) we'll be replaced with the appname so the url matches the right one.
260                #If other site has other url naming convention it'll be mandatory to define it with the appropiate replacements
261                infoList={'appimg':'https://bintray.com/probono/AppImages/$(appname)'}
262                self.descDict={}
263
264                for repoType,repoTypeList in repoList.items():
265                        infoPage=infoList[repoType]
266                        outdir=self.bundleDir+'/'+repoType+'/'
267                        if self._chk_bundle_dir(outdir):
268                                for repo in repoTypeList:
269                                        self._debug(("Fetching repo %s")%(repo))
270                                        print (_("Fetching %s catalogue: "+progressBar)%repoType,end="\r")
271                                        progressBar=progressBar+"#"
272                                        print (_("Fetching %s catalogue: "+progressBar)%repoType,end="\r")
273                                        applist=self._generate_applist(self._fetch_repo(repo))
274                                        progressBar=progressBar+"##"
275                                        print (_("Fetching %s catalogue: "+progressBar)%repoType,end="\r")
276                                        self._debug("Processing info...")
277                                        self._th_generate_xml_catalog(applist,outdir,infoPage,repoType,progressBar)
278                                        self._debug("Fetched repo "+repo)
279                        else:
280                                print("permission denied")
281                return(True)
282
283        def _chk_bundle_dir(self,outdir):
284                msg_status=True
285                if not os.path.isdir(outdir):
286                        try:
287                                os.makedirs(outdir)
288                        except:
289                                msg_status=False
290                return(os.access(outdir,os.W_OK|os.R_OK|os.X_OK|os.F_OK))
291
292        def _fetch_repo(self,repo):
293                with urllib.request.urlopen(repo) as f:
294                        content=(f.read().decode('utf-8'))
295                return(content)
296
297        def _generate_applist(self,content):
298                garbageList=[]
299                applist=[]
300                garbageList=content.split(' ')
301                for garbageLine in garbageList:
302                        if garbageLine.endswith('AppImage"'):
303                                app=garbageLine.replace('href=":','')
304                                applist.append(app.replace('"',''))
305                return(applist)
306
307        def _get_description(self,appName,infoPage):
308                desc=''
309                if '$(appname)' in infoPage:
310                        infoPage=infoPage.replace('$(appname)',appName)
311                self._debug("Getting description from "+infoPage)
312                try:
313                        with urllib.request.urlopen(infoPage) as f:
314                                content=(f.read().decode('utf-8'))
315                                soup=BeautifulSoup(content,"html.parser")
316                                descDiv=soup.findAll('div', attrs={ "class" : "description-text"})
317                        if len(descDiv)>0:
318                                desc=descDiv[0].text
319                                desc=desc.replace(':','.')
320                                desc=desc.replace('&','&')
321                except Exception as e:
322                        print("Can't get description from "+infoPage)
323                        print(str(e))
324                        pass
325                return(desc)
326
327        def _th_generate_xml_catalog(self,applist,outdir,infoPage,repoType,progressBar=''):
328                CURSOR_UP='\033[F'
329                ERASE_LINE='\033[K'
330                oldName=''
331                oldDesc=''
332                maxconnections = 10
333                semaphore = threading.BoundedSemaphore(value=maxconnections)
334                randomList = list(applist)
335                random.shuffle(randomList)
336                lenAppList=len(randomList)
337                inc=30/lenAppList
338                print (CURSOR_UP)
339                for app in randomList:
340                        th=threading.Thread(target=self._th_write_xml, args = (app,outdir,infoPage,semaphore,inc))
341                        th.start()
342                os.system('setterm -cursor off')
343                while threading.active_count()>2: #Discard both main and own threads
344                        for i in range(len(progressBar),int(self.progress)):
345                                progressBar='#'+progressBar
346#                       print (CURSOR_UP)
347                        print (_("Fetching %s catalogue: "+progressBar)%repoType,end="\r")
348                os.system('setterm -cursor on')
349                print('')
350
351        def _th_write_xml(self,app,outdir,infoPage,semaphore,inc):
352                semaphore.acquire()
353                lock=threading.Lock()
354                nameSplitted=app.split('-')
355                name=nameSplitted[0]
356                version=nameSplitted[1]
357                arch=nameSplitted[2]
358                filename=outdir+name+"_"+version+".appdata.xml"
359                self._debug("checking if we need to download "+filename)
360                if not os.path.isfile(filename):
361                        self._debug("Generating "+app+" xml")
362                        f=open(filename,'w')
363                        f.write('<?xml version="1.0" encoding="UTF-8"?>'+"\n")
364                        f.write("<components version=\"0.10\">\n")
365                        f.write("<component  type=\"desktop-application\">\n")
366                        f.write("  <id>"+app.lower()+"</id>\n")
367                        f.write("  <pkgname>"+app+"</pkgname>\n")
368                        f.write("  <name>"+name+"</name>\n")
369                        f.write("  <summary>"+name+" AppImage Bundle</summary>\n")
370                        f.write("  <metadata_license>CC0-1.0</metadata_license>\n")
371                        f.write("  <provides><binary>"+app+"</binary></provides>\n")
372                        f.write("  <releases>\n")
373                        f.write("  <release version=\""+version+"\" timestamp=\"1408573857\"></release>\n")
374                        f.write("  </releases>\n")
375                        f.write("  <launchable type=\"desktop-id\">"+name+".desktop</launchable>\n")
376                        with lock:
377                                if name in self.descDict.keys():
378                                        description=self.descDict[name]
379                                else:
380                                        description=self._get_description(name,infoPage)
381                                        self.descDict.update({name:description})
382                        f.write("  <description><p>This is an AppImage bundle of app "+name+". It hasn't been tested by our developers and comes from a 3rd party dev team. Please use it carefully.</p><p>"+description+"</p></description>\n")
383                        f.write("  <bundle type=\"appimage\">"+app+"</bundle>\n")
384                        f.write("  <keywords>\n")
385                        f.write("       <keyword>"+name+"</keyword>\n")
386                        f.write("       <keyword>appimage</keyword>\n")
387                        f.write("  </keywords>\n")
388                        f.write("  <categories>\n")
389                        f.write("       <category>AppImage</category>\n")
390                        f.write("       <category>GTK</category>\n")
391                        f.write("  </categories>\n")
392                        f.write("<icon type=\"cached\">"+name+"_"+name+".png</icon>\n")
393                        f.write("</component>\n")
394                        f.write("</components>\n")
395                        f.close()
396                with lock:
397                        self.progress=self.progress+inc
398                semaphore.release()
Note: See TracBrowser for help on using the repository browser.