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

Last change on this file since 7432 was 7432, checked in by Juanma, 2 years ago

fix wrong update

File size: 19.7 KB
Line 
1#The name of the main class must match the file name in lowercase
2import re
3import urllib
4from urllib.request import Request
5from urllib.request import urlretrieve
6import shutil
7import json
8import os
9import sys
10import threading
11import queue
12import time
13import random
14import gi
15from gi.repository import Gio
16gi.require_version('AppStreamGlib', '1.0')
17from gi.repository import AppStreamGlib as appstream
18from bs4 import BeautifulSoup
19#from subprocess import call
20
21class appimagemanager:
22        def __init__(self):
23                self.dbg=True
24                self.progress=0
25                self.partial_progress=0
26                self.plugin_actions={'install':'appimage','remove':'appimage','pkginfo':'appimage','load':'appimage'}
27                self.result={}
28                self.result['data']={}
29                self.result['status']={}
30                self.cache_dir=os.getenv("HOME")+"/.cache/lliurex-store"
31                self.icons_dir=self.cache_dir+"/icons"
32                self.bundles_dir=self.cache_dir+"/xmls/appimage"
33                self.bundle_types=['appimg']
34#               self.appimage_dir=os.getenv("HOME")+"/.lliurex-store/appimg"
35                self.appimage_dir=os.getenv("HOME")+"/.local/bin"
36                #To get the description of an app we must go to a specific url defined in url_info.
37                #$(appname) we'll be replaced with the appname so the url matches the right one.
38                #If other site has other url naming convention it'll be mandatory to define it with the appropiate replacements
39                self.repos={'appimagehub':{'type':'json','url':'https://appimage.github.io/feed.json','url_info':''}}
40                #Appimges not stored in a repo must be listed in this file, providing the download url and the info url (if there's any)
41                self.external_appimages="/usr/share/lliurex-store/files/external_appimages.json"
42                self.locale=['ca_ES@valencia','ca@valencia','qcv','ca','ca_ES','es_ES','es','en_US','en_GB','en','C']
43                self.disabled=False
44                self.icon_cache_enabled=True
45                self.image_cache_enabled=True
46                self.apps_for_store=queue.Queue()
47        #def __init__
48
49        def set_debug(self,dbg=True):
50                self.dbg=dbg
51                self._debug ("Debug enabled")
52        #def set_debug
53
54        def _debug(self,msg=''):
55                if self.dbg:
56                        print ('DEBUG appimage: %s'%msg)
57        #def debug
58
59        def register(self):
60                return(self.plugin_actions)
61
62        def enable(self,state=False):
63                self.disable=state
64
65        def execute_action(self,action,applist=None,store=None):
66                if store:
67                        self.store=store
68                else:
69                        self.store=appstream.Store()
70                self.appimage_store=appstream.Store()
71                self.progress=0
72                self.result['status']={'status':-1,'msg':''}
73                self.result['data']=[]
74                self.threads=[]
75                dataList=[]
76                if self.disabled:
77                        self._set_status(9)
78                        self.result['data']=self.store
79                else:
80                        self._chk_installDir()
81                        if action=='load':
82                                self._load_appimage_store()
83#                               self.result['data']=self._load_appimage_store()
84                                #wait till threads end (if any)
85                                self._debug("Ending threads...")
86#                               for app in self.apps_for_store:
87                                for th in threading.enumerate():
88                                        if th.is_alive():
89                                                try:
90                                                        th.join(5)
91                                                except:
92                                                        pass
93                                while not self.apps_for_store.empty():
94                                        app=self.apps_for_store.get()
95                                        self.store.add_app(app)
96                                self.result['data']=self.store
97                        else:
98                                for app_info in applist:
99                                        self.partial_progress=0
100                                        if action=='install':
101                                                dataList.append(self._install_appimage(app_info))
102                                        if action=='remove':
103                                                dataList.append(self._remove_appimage(app_info))
104                                        if action=='pkginfo':
105                                                dataList.append(self._get_info(app_info))
106                                        self.progress+=int(self.partial_progress/len(applist))-1
107                                self.result['data']=list(dataList)
108                self.progress=100
109                return(self.result)
110
111        def _set_status(self,status,msg=''):
112                self.result['status']={'status':status,'msg':msg}
113        #def _set_status
114
115        def _callback(self,partial_size=0,total_size=0):
116                limit=99
117                if partial_size!=0 and total_size!=0:
118                        inc=round(partial_size/total_size,2)*100
119                        self.progress=inc
120                else:
121                        inc=1
122                        margin=limit-self.progress
123                        inc=round(margin/limit,3)
124                        self.progress=(self.progress+inc)
125                if (self.progress>limit):
126                        self.progress=limit
127
128        def _chk_installDir(self):
129                msg_status=True
130                if not os.path.isdir(self.appimage_dir):
131                        try:
132                                os.makedirs(self.appimage_dir)
133                        except:
134                                msg_status=False
135                return msg_status                               
136
137        def _install_appimage(self,app_info):
138                app_info=self._get_info(app_info)
139                self._debug("Installing %s"%app_info)
140                if app_info['state']=='installed':
141                        self._set_status(4)
142                else:
143#                       appimage_url=app_info['installerUrl']+'/'+app_info['package']
144                        appimage_url=app_info['channel_releases']['appimage'][0]
145                        self._debug("Downloading "+appimage_url)
146                        dest_path=self.appimage_dir+'/'+app_info['package']
147                        if appimage_url:
148                                try:
149                                        req=Request(appimage_url, headers={'User-Agent':'Mozilla/5.0'})
150                                        with urllib.request.urlopen(req) as response, open(dest_path, 'wb') as out_file:
151                                                bf=16*1024
152                                                acumbf=0
153#                                               print("Response: %s"%response.info())
154                                                app_size=int(response.info()['Content-Length'])
155#                                               print("APP SIZE: %s"%app_size)
156                                                while True:
157                                                        if acumbf>=app_size:
158                                                            break
159                                                        shutil.copyfileobj(response, out_file,bf)
160                                                        acumbf=acumbf+bf
161                                                        self._callback(acumbf,app_size)
162                                        st = os.stat(dest_path)
163                                        os.chmod(dest_path, st.st_mode | 0o111)
164                                        self._set_status(0)
165                                except Exception as e:
166                                        print(e)
167                                        self._set_status(5)
168                        else:
169                                self._set_status(12)
170                return app_info
171        #def _install_appimage
172
173        def _remove_appimage(self,app_info):
174                self._debug("Removing "+app_info['package'])
175                if os.path.isfile(self.appimage_dir+'/'+app_info['package']):
176                        try:
177                                call([self.appimage_dir+"/"+app_info['package'], "--remove-appimage-desktop-integration"])
178                        except:
179                                pass
180                        try:
181                                os.remove(self.appimage_dir+"/"+app_info['package'])
182                                self._set_status(0)
183                        except:
184                                self._set_status(6)
185                return(app_info)
186        #def _remove_appimage
187
188        def _load_appimage_store(self,store=None):
189                self._get_bundles_catalogue()
190                if os.path.exists(self.bundles_dir):
191                        for bundle_type in self.bundle_types:
192                                self._debug("Loading %s catalog"%bundle_type)
193                                store=self._generic_file_load(self.bundles_dir+'/'+bundle_type,store)
194                return(store)
195        #def load_bundles_catalog(self)
196       
197        def _generic_file_load(self,target_path,store):
198                icon_path='/usr/share/icons/hicolor/128x128'
199                if not os.path.isdir(target_path):
200                        os.makedirs(target_path)
201                files=os.listdir(target_path)
202                for target_file in os.listdir(target_path):
203                        if target_file.endswith('appdata.xml'):
204                                store_path=Gio.File.new_for_path(target_path+'/'+target_file)
205                                self._debug("Adding file "+target_path+'/'+target_file)
206                                try:
207                                        store.from_file(store_path,icon_path,None)
208                                except Exception as e:
209                                        self._debug("Couldn't add file "+target_file+" to store")
210                                        self._debug("Reason: "+str(e))
211                return(store)
212        #def _generic_file_load
213
214        def _get_bundles_catalogue(self):
215                applist=[]
216                appdict={}
217                all_apps=[]
218                outdir=self.bundles_dir+'/appimg/'
219                #Load repos
220                for repo_name,repo_info in self.repos.items():
221                        if not os.path.isdir(self.bundles_dir):
222                                try:
223                                        os.makedirs(self.bundles_dir)
224                                except:
225                                        self._debug("appImage catalogue could not be fetched: Permission denied")
226                        self._debug("Fetching repo %s"%repo_info['url'])
227                        if repo_info['type']=='json':
228                                applist=self._process_appimage_json(self._fetch_repo(repo_info['url']),repo_name)
229
230                        self._debug("Fetched repo "+repo_info['url'])
231                        self._th_generate_xml_catalog(applist,outdir,repo_info['url_info'],repo_info['url'],repo_name)
232                        all_apps.extend(applist)
233                #Load external apps
234                for app_name,app_info in self._get_external_appimages().items():
235                        if os.path.isdir(self.bundles_dir):
236                                appinfo=self._init_appinfo()
237                                appinfo['name']=app_info['url'].split('/')[-1]
238                                appinfo['package']=app_info['url'].split('/')[-1]
239                                appinfo['homepage']='/'.join(app_info['url'].split('/')[0:-1])
240                                self._debug("Fetching external appimage %s"%app_info['url'])
241                                appinfo['bundle']='appimage'
242                                applist=[appinfo]
243                                self._th_generate_xml_catalog(applist,outdir,app_info['url_info'],app_info['url'],app_name)
244                                self._debug("Fetched appimage "+app_info['url'])
245                                all_apps.extend(applist)
246                        else:
247                                self._debug("External appImage could not be fetched: Permission denied")
248                self._debug("Removing old entries...")
249#               self._clean_bundle_catalogue(all_apps,outdir)
250                return(True)
251        #def _get_bundles_catalogue
252       
253        def _fetch_repo(self,repo):
254                req=Request(repo, headers={'User-Agent':'Mozilla/5.0'})
255                with urllib.request.urlopen(req) as f:
256                        content=(f.read().decode('utf-8'))
257               
258                return(content)
259        #def _fetch_repo
260       
261        def _get_external_appimages(self):
262                external_appimages={}
263                if os.path.isfile(self.external_appimages):
264                        try:
265                                with open(self.external_appimages) as appimages:
266                                        external_appimages=json.load(appimages)
267                        except:
268                                self._debug("Can't load %s"%self.external_appimages)
269                self._debug(external_appimages)
270                return external_appimages
271        #def _get_external_appimages
272       
273        def _process_appimage_json(self,data,repo_name):
274                applist=[]
275                json_data=json.loads(data)
276                if 'items' in json_data.keys():
277                        for appimage in json_data['items']:
278                                appinfo=self._th_process_appimage(appimage)
279                                if appinfo:
280                                        applist.append(appinfo)
281                return (applist)
282        #_process_appimage_json
283
284        def _th_process_appimage(self,appimage):
285                appinfo=None
286                releases=[]
287                if 'links' in appimage.keys():
288                        if appimage['links']:
289                                appinfo=self.load_json_appinfo(appimage)
290#Deprecated. appImage releases will be load on the info stage
291#                       releases=self._get_releases_from_json(appimage)
292#                       if releases:
293#                               appinfo['releases']=releases
294#                               for release in releases:
295#                                       #Release has the direct download url
296#                                       tmp_release=release.split('/')
297#                                       tmp_appinfo=appinfo.copy()
298#                                       rel_number=tmp_release[-2]
299#                                       rel_name=tmp_release[-1].lower().replace('.appimage','')
300#                                       self._debug("Release: %s"%release)
301#                                       tmp_appinfo['name']=rel_name
302#                                       tmp_appinfo['package']=tmp_release[-1]
303#                                       tmp_appinfo['homepage']='/'.join(tmp_release[0:-1])
304#                                       self.queue.put(tmp_appinfo)
305                return(appinfo)
306        #def _th_process_appimage
307
308        def load_json_appinfo(self,appimage):
309                appinfo=self._init_appinfo()
310                appinfo['name']=appimage['name']
311                appinfo['package']=appimage['name']
312                if 'license' in appimage.keys():
313                        appinfo['license']=appimage['license']
314                appinfo['summary']=''
315                if 'description' in appimage.keys():
316                        appinfo['description']=appimage['description']
317                if 'categories' in appimage.keys():
318                        appinfo['categories']=appimage['categories']
319                if 'icon' in appimage.keys():
320                        appinfo['icon']=appimage['icon']
321                if 'icons' in appimage.keys():
322                        self._debug("Loading icon %s"%appimage['icons'])
323                        if appimage['icons']:
324                                self._debug("Loading icon %s"%appimage['icons'][0])
325                                appinfo['icon']=appimage['icons'][0]
326                if 'screenshots' in appimage.keys():
327                        appinfo['thumbnails']=appimage['screenshots']
328                if 'links' in appimage.keys():
329                        if appimage['links']:
330                                for link in appimage['links']:
331                                        if 'url' in link.keys() and link['type']=='Download':
332                                                appinfo['installerUrl']=link['url']
333                if 'authors' in appimage.keys():
334                        if appimage['authors']:
335                                for author in appimage['authors']:
336                                        if 'url' in author.keys():
337                                                self._debug("Author: %s"%author['url'])
338                                                appinfo['homepage']=author['url']
339                else:
340                        appinfo['homepage']='/'.join(appinfo['installerUrl'].split('/')[0:-1])
341                appinfo['bundle']=['appimage']
342                return appinfo
343        #def load_json_appinfo
344
345        def _th_generate_xml_catalog(self,applist,outdir,info_url,repo,repo_name):
346                maxconnections = 2
347                threads=[]
348                semaphore = threading.BoundedSemaphore(value=maxconnections)
349                random_applist = list(applist)
350                random.shuffle(random_applist)
351                for app in applist:
352                        th=threading.Thread(target=self._th_write_xml, args = (app,outdir,info_url,repo,repo_name,semaphore))
353                        threads.append(th)
354                        th.start()
355        #def _th_generate_xml_catalog
356
357        def     _th_write_xml(self,appinfo,outdir,info_url,repo,repo_name,semaphore):
358                semaphore.acquire()
359                self._add_appimage(appinfo)
360                semaphore.release()
361        #def _th_write_xml
362
363#       def _add_appimage(self,appinfo,repo_info):
364        def _add_appimage(self,appinfo):
365                #Search in local store for the app
366                sw_new=False
367                app=appstream.App()
368                app_orig=self.store.get_app_by_pkgname(appinfo['package'].lower())
369                if not app_orig:
370                        app_orig=self.store.get_app_by_id(appinfo['package'].lower()+".desktop")
371                if app_orig:
372                        self._debug("Extending app %s"%appinfo['package'])
373                        app=self._copy_app_from_appstream(app_orig,app)
374                else:
375                        self._debug("Generating new %s"%appinfo['package'])
376                app.set_id("appimagehub.%s"%appinfo['name'].lower()+'.appimage')
377                app.set_id_kind=appstream.IdKind.DESKTOP
378                sw_new=True
379
380                icon=appstream.Icon()
381                screenshot=appstream.Screenshot()
382                app.set_name("C",appinfo['name']+".appimage")
383                app.add_pkgname(appinfo['package'].lower()+".appimage")
384                if appinfo['license']:
385                        app.set_project_license(appinfo['license'])
386                bundle=appstream.Bundle()
387                bundle.set_kind(bundle.kind_from_string('APPIMAGE'))
388                bundle.set_id(appinfo['package']+'.appimage')
389#               if app_orig:
390#                       app_orig.add_bundle(bundle)
391                app.add_bundle(bundle)
392                app.add_keyword("C","appimage")
393                app.add_category("appimage")
394                app.add_url(appstream.UrlKind.UNKNOWN,appinfo['installerUrl'])
395                app.add_url(appstream.UrlKind.HOMEPAGE,appinfo['homepage'])
396                if sw_new:
397                        app.add_keyword("C",appinfo['package'])
398                        app.set_name("C",appinfo['name']+".appimage")
399                        description="This is an AppImage bundle of app %s. It hasn't been tested by our developers and comes from a 3rd party dev team. Please use it carefully.\n%s"%(appinfo['name'],appinfo['description'])
400                        summary=' '.join(list(description.split(' ')[:8]))
401                        if len(description.split(' '))>8:
402                                summary+="... "
403                        app.set_description("C",description)
404                        app.set_comment("C",summary)
405                        for category in appinfo['categories']:
406                                app.add_category(category)
407                if appinfo['icon']:
408                        if self.icon_cache_enabled:
409                                icon.set_kind(appstream.IconKind.LOCAL)
410                                icon.set_name(self._download_file(appinfo['icon'],appinfo['name'],self.icons_dir))
411                        else:
412                                icon.set_kind(appstream.IconKind.REMOTE)
413                                icon.set_name(pkg.get_icon())
414                                icon.set_url(pkg.get_icon())
415                        app.add_icon(icon)
416                if appinfo['thumbnails']:
417                        img=appstream.Image()
418                        if not appinfo['thumbnails'][0].startswith('http'):
419                                        appinfo['screenshot']=appinfo['thumbnails'][0]
420                                        appinfo['screenshot']="https://appimage.github.io/database/%s"%appinfo['screenshot']
421                        img.set_kind(appstream.ImageKind.SOURCE)
422                        img.set_url(appinfo['screenshot'])
423                        screenshot.add_image(img)
424                        app.add_screenshot(screenshot)
425                #Adds the app to the store
426#               self._debug("Adding %s"%appinfo['package'])
427                self.apps_for_store.put(app)
428                if not os.path.isfile(self.bundles_dir+'/'+app.get_id_filename()):
429                        gioFile=Gio.File.new_for_path('%s/%s.xml'%(self.bundles_dir,app.get_id_filename()))
430                        app.to_file(gioFile)
431        #def _add_appimage
432
433        def _copy_app_from_appstream(self,app_orig,app):
434                app.set_id("appimage."+app_orig.get_id())
435                for category in app_orig.get_categories():
436                        app.add_category(category)
437                for screenshot in app_orig.get_screenshots():
438                        app.add_screenshot(screenshot)
439                for icon in app_orig.get_icons():
440                        app.add_icon(icon)
441                for localeItem in self.locale:
442                        if app_orig.get_name(localeItem):
443                                app.set_name(localeItem,app_orig.get_name(localeItem)+".appimage")
444                        if app_orig.get_description(localeItem):
445                                app.set_description(localeItem,app_orig.get_description(localeItem))
446                        if app_orig.get_comment(localeItem):
447                                app.set_comment(localeItem,app_orig.get_comment(localeItem))
448                app.set_origin(app_orig.get_origin())
449                return app
450
451        def _clean_bundle_catalogue(self,applist,outdir):
452                xml_files_list=[]
453                applist=[item.lower() for item in applist]
454                for xml_file in os.listdir(outdir):
455                        if xml_file.endswith('appdata.xml'):
456                                xml_files_list.append(xml_file.lower().replace('appdata.xml','appimage'))
457       
458                if xml_files_list:
459                        xml_discard_list=list(set(xml_files_list).difference(applist))
460                        for discarded_file in xml_discard_list:
461                                os.remove(outdir+'/'+discarded_file.replace('appimage','appdata.xml'))
462        #def _clean_bunlde_catalogue
463
464        def _download_file(self,url,app_name,dest_dir):
465#               target_file=self.icons_folder+'/'+app_name+".png"
466                target_file=dest_dir+'/'+app_name+".png"
467                if not url.startswith('http'):
468                        url="https://appimage.github.io/database/%s"%url
469                if not os.path.isfile(target_file):
470#                       shutil.copy("/usr/share/icons/hicolor/128x128/apps/lliurex-store.png",target_file)
471#                       if not os.fork():
472                        if not os.path.isfile(target_file):
473                                self._debug("Downloading %s to %s"%(url,target_file))
474                                try:
475                                        with urllib.request.urlopen(url) as response, open(target_file, 'wb') as out_file:
476                                                bf=16*1024
477                                                acumbf=0
478                                                file_size=int(response.info()['Content-Length'])
479                                                while True:
480                                                        if acumbf>=file_size:
481                                                            break
482                                                        shutil.copyfileobj(response, out_file,bf)
483                                                        acumbf=acumbf+bf
484                                        st = os.stat(target_file)
485                                except Exception as e:
486                                        self._debug("Unable to download %s"%url)
487                                        self._debug("Reason: %s"%e)
488                                        target_file=''
489#                               os._exit(0)
490                return(target_file)
491        #def _download_file
492       
493        def _chk_bundle_dir(self,outdir):
494                msg_status=True
495                if not os.path.isdir(outdir):
496                        try:
497                                os.makedirs(outdir)
498                        except Exception as e:
499                                msg_status=False
500                                print(e)
501                return(os.access(outdir,os.W_OK|os.R_OK|os.X_OK|os.F_OK))
502        #def _chk_bundle_dir
503       
504        def _init_appinfo(self):
505                appInfo={'appstream_id':'',\
506                'id':'',\
507                'name':'',\
508                'version':'',\
509                'channel_releases':{},\
510                'component':'',\
511                'package':'',\
512                'license':'',\
513                'summary':'',\
514                'description':'',\
515                'categories':[],\
516                'icon':'',\
517                'screenshot':'',\
518                'thumbnails':[],\
519                'video':'',\
520                'homepage':'',\
521                'installerUrl':'',\
522                'state':'',\
523                'depends':'',\
524                'kudos':'',\
525                'suggests':'',\
526                'extraInfo':'',\
527                'size':'',\
528                'bundle':'',\
529                'updatable':'',\
530                }
531                return(appInfo)
532        #def _init_appinfo
533       
534        def _get_info(self,app_info):
535                if app_info['installerUrl']:
536                        app_info['channel_releases']={'appimage':[]}
537                        app_info['channel_releases']['appimage']=self._get_releases(app_info)
538                app_info['state']='available'
539                if os.path.isfile(self.appimage_dir+'/'+app_info['package']):
540                        app_info['state']='installed'
541                #Get size
542                if 'appimage' in app_info['channel_releases'].keys():
543                        if app_info['channel_releases']['appimage'][0]:
544                                appimage_url=app_info['channel_releases']['appimage'][0]
545                                dest_path=self.appimage_dir+'/'+app_info['package']
546                                if appimage_url:
547                                        try:
548                                                with urllib.request.urlopen(appimage_url) as response:
549                                                        app_info['size']=(response.info()['Content-Length'])
550                                        except:
551                                                app_info['size']=0
552                        else:
553                                app_info['size']=0
554                        #Version (unaccurate aprox)
555                        app_info['version']=app_info['channel_releases']['appimage'][0].split('/')[-2]
556#                       try:
557#                               app_info['version']="%s.%s"%(version.split('.')[1],version.split('.')[2])
558#                       except:
559#                               app_info['version']="%s"%version.split('_')[-1]
560
561                self._set_status(0)
562                self.partial_progress=100
563                return(app_info)
564        #def _get_info
565
566        def _get_releases(self,app_info):
567                releases=[]
568                releases_page=''
569                self._debug("Info url: %s"%app_info['installerUrl'])
570                url_source=""
571                try:
572                        if 'github' in app_info['installerUrl']:
573                                releases_page="https://github.com"
574                        if 'gitlab' in app_info['installerUrl']:
575                                releases_page="https://gitlab.com"
576                        if 'opensuse' in app_info['installerUrl'].lower():
577                                releases_page=""
578                                url_source="opensuse"
579#                               app_info['installerUrl']=app_info['installerUrl']+"/download"
580
581                        with urllib.request.urlopen(app_info['installerUrl']) as f:
582                                content=(f.read().decode('utf-8'))
583                                soup=BeautifulSoup(content,"html.parser")
584                                package_a=soup.findAll('a', attrs={ "href" : re.compile(r'.*\.[aA]pp[iI]mage$')})
585
586                                for package_data in package_a:
587                                        if url_source=="opensuse":
588                                                package_name=package_data.findAll('a', attrs={"class" : "mirrorbrain-btn"})
589                                        else:
590                                                package_name=package_data.findAll('strong', attrs={ "class" : "pl-1"})
591                                        package_link=package_data['href']
592                                        if releases_page or url_source:
593                                                package_link=releases_page+package_link
594                                                releases.append(package_link)
595                                                self._debug("Link: %s"%package_link)
596                except Exception as e:
597                        print(e)
598                return releases
599        #def _get_releases
600       
Note: See TracBrowser for help on using the repository browser.