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

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

launch appimages and snaps from gui

File size: 15.9 KB
Line 
1#The name of the main class must match the file name in lowercase
2import urllib
3from urllib.request import Request
4from urllib.request import urlretrieve
5import shutil
6import json
7import os
8from subprocess import call
9import sys
10import threading
11from bs4 import BeautifulSoup
12import random
13import time
14import gi
15from gi.repository import Gio
16gi.require_version('AppStreamGlib', '1.0')
17from gi.repository import AppStreamGlib as appstream
18
19class appimagemanager:
20        def __init__(self):
21                self.dbg=False
22                self.progress=0
23                self.partial_progress=0
24                self.plugin_actions={'install':'appimage','remove':'appimage','pkginfo':'appimage','load':'appimage'}
25                self.result={}
26                self.result['data']={}
27                self.result['status']={}
28                self.conf_dir=os.getenv("HOME")+"/.cache/lliurex-store"
29                self.icons_dir=self.conf_dir+"/icons"
30                self.bundles_dir=self.conf_dir+"/bundles"
31                self.bundle_types=['appimg']
32                self.appimage_dir=os.getenv("HOME")+"/.lliurex-store/appimg"
33                #To get the description of an app we must go to a specific url defined in url_info.
34                #$(appname) we'll be replaced with the appname so the url matches the right one.
35                #If other site has other url naming convention it'll be mandatory to define it with the appropiate replacements
36                self.repos={'probono':{'url':'https://dl.bintray.com/probono/AppImages', 'url_info':'https://bintray.com/probono/AppImages/$(appname)'}}
37                #Appimges not stored in a repo must be listed in this file, providing the download url and the info url (if there's any)
38                self.external_appimages="/usr/share/lliurex-store/files/external_appimages.json"
39                self.disabled=False
40                self.count=0
41        #def __init__
42
43        def set_debug(self,dbg=True):
44                self.dbg=dbg
45                self._debug ("Debug enabled")
46        #def set_debug
47
48        def _debug(self,msg=''):
49                if self.dbg:
50                        print ('DEBUG appimage: %s'%msg)
51        #def debug
52
53        def register(self):
54                return(self.plugin_actions)
55
56        def enable(self,state=False):
57                self.disable=state
58
59        def execute_action(self,action,applist=None,store=None):
60                if store:
61                        self.store=store
62                else:
63                        self.store=appstream.Store()
64                self.progress=0
65                self.result['status']={'status':-1,'msg':''}
66                self.result['data']=''
67                dataList=[]
68                if self.disabled:
69                        self._set_status(9)
70                        self.result['data']=self.store
71                else:
72                        self._chk_installDir()
73                        if action=='load':
74                                self.result['data']=self._load_appimage_store(self.store)
75                        else:
76                                for app_info in applist:
77                                        self.partial_progress=0
78                                        if action=='install':
79                                                dataList.append(self._install_appimage(app_info))
80                                        if action=='remove':
81                                                dataList.append(self._remove_appimage(app_info))
82                                        if action=='pkginfo':
83                                                dataList.append(self._get_info(app_info))
84                                        self.progress+=int(self.partial_progress/len(applist))
85                                self.result['data']=list(dataList)
86                self.progress=100
87                return(self.result)
88
89        def _set_status(self,status,msg=''):
90                self.result['status']={'status':status,'msg':msg}
91        #def _set_status
92
93        def _callback(self,partial_size=0,total_size=0):
94                limit=99
95                if partial_size!=0 and total_size!=0:
96                        inc=round(partial_size/total_size,2)*100
97                        self.progress=inc
98                else:
99                        inc=1
100                        margin=limit-self.progress
101                        inc=round(margin/limit,3)
102                        self.progress=(self.progress+inc)
103                if (self.progress>limit):
104                        self.progress=limit
105
106        def _chk_installDir(self):
107                msg_status=True
108                if not os.path.isdir(self.appimage_dir):
109                        try:
110                                os.makedirs(self.appimage_dir)
111                        except:
112                                msg_status=False
113                return msg_status                               
114
115        def _install_appimage(self,app_info):
116                app_info=self._get_info(app_info)
117                self._debug("Installing %s"%app_info)
118                if app_info['state']=='installed':
119                        self._set_status(4)
120                else:
121                                #                       appimage_url=self.repository_url+'/'+app_info['package']
122                        appimage_url=app_info['homepage']+'/'+app_info['package']
123                        self._debug("Downloading "+appimage_url)
124                        dest_path=self.appimage_dir+'/'+app_info['package']
125                        if appimage_url:
126                                try:
127                                        req=Request(appimage_url, headers={'User-Agent':'Mozilla/5.0'})
128                                        with urllib.request.urlopen(req) as response, open(dest_path, 'wb') as out_file:
129                                                bf=16*1024
130                                                acumbf=0
131#                                               print("Response: %s"%response.info())
132                                                app_size=int(response.info()['Content-Length'])
133#                                               print("APP SIZE: %s"%app_size)
134                                                while True:
135                                                        if acumbf>=app_size:
136                                                            break
137                                                        shutil.copyfileobj(response, out_file,bf)
138                                                        acumbf=acumbf+bf
139                                                        self._callback(acumbf,app_size)
140                                        st = os.stat(dest_path)
141                                        os.chmod(dest_path, st.st_mode | 0o111)
142                                        self._set_status(0)
143                                except Exception as e:
144                                        print(e)
145                                        self._set_status(5)
146                        else:
147                                self._set_status(12)
148                return app_info
149        #def _install_appimage
150
151        def _remove_appimage(self,app_info):
152                self._debug("Removing "+app_info['package'])
153                if os.path.isfile(self.appimage_dir+'/'+app_info['package']):
154                        try:
155                                call([self.appimage_dir+"/"+app_info['package'], "--remove-appimage-desktop-integration"])
156                        except:
157                                pass
158                        try:
159                                os.remove(self.appimage_dir+"/"+app_info['package'])
160                                self._set_status(0)
161                        except:
162                                self._set_status(6)
163                return(app_info)
164        #def _remove_appimage
165
166        def _get_info(self,app_info):
167                app_info['state']='available'
168                if os.path.isfile(self.appimage_dir+'/'+app_info['package']):
169                        app_info['state']='installed'
170                #Get size
171                appimage_url=app_info['homepage']+'/'+app_info['package']
172                dest_path=self.appimage_dir+'/'+app_info['package']
173                if appimage_url:
174                        try:
175                                with urllib.request.urlopen(appimage_url) as response:
176                                        app_info['size']=(response.info()['Content-Length'])
177                        except:
178                                app_info['size']=0
179                self._set_status(0)
180                self.partial_progress=100
181                return(app_info)
182        #def _get_info
183
184        def _load_appimage_store(self,store):
185                self._download_bundles_catalogue()
186                if os.path.exists(self.bundles_dir):
187                        for bundle_type in self.bundle_types:
188                                self._debug("Loading %s catalog"%bundle_type)
189                                store=self._generic_file_load(self.bundles_dir+'/'+bundle_type,store)
190                return(store)
191        #def load_bundles_catalog(self)
192       
193        def _generic_file_load(self,target_path,store):
194                icon_path='/usr/share/icons/hicolor/128x128'
195                if not os.path.isdir(target_path):
196                        os.makedirs(target_path)
197                files=os.listdir(target_path)
198                for target_file in os.listdir(target_path):
199                        if target_file.endswith('appdata.xml'):
200                                store_path=Gio.File.new_for_path(target_path+'/'+target_file)
201                                self._debug("Adding file "+target_path+'/'+target_file)
202                                try:
203                                        store.from_file(store_path,icon_path,None)
204                                except Exception as e:
205                                        self._debug("Couldn't add file "+target_file+" to store")
206                                        self._debug("Reason: "+str(e))
207                return(store)
208        #def _generic_file_load
209
210        def _download_bundles_catalogue(self):
211                CURSOR_UP='\033[F'
212                ERASE_LINE='\033[K'
213                content=''
214                applist=[]
215                progress_bar="#"
216                self.descriptions_dict={}
217                all_apps=[]
218                outdir=self.bundles_dir+'/appimg/'
219                #Load repos
220                for repo_name,repo_info in self.repos.items():
221                        if self._chk_bundle_dir(outdir):
222                                self._debug("Fetching repo %s"%repo_info['url'])
223##                                      print (("Fetching %s catalogue: "+progress_bar)%repo_type,end="\r")
224##                                      progress_bar=progress_bar+"#"
225##                                      print (("Fetching %s catalogue: "+progress_bar)%repo_type,end="\r")
226                                applist=self._generate_applist(self._fetch_repo(repo_info['url']),repo_name)
227##                                      progress_bar=progress_bar+"##"
228##                                      print (("Fetching %s catalogue: "+progress_bar)%repo_type,end="\r")
229                                self._debug("Processing info...")
230                                self._th_generate_xml_catalog(applist,outdir,repo_info['url_info'],repo_info['url'],repo_name,progress_bar)
231                                self._debug("Fetched repo "+repo_info['url'])
232                                all_apps.extend(applist)
233                        else:
234                                        self._debug("appImage catalogue could not be fetched: Permission denied")
235                #Load external apps
236                for app_name,app_info in self._get_external_appimages().items():
237                        if self._chk_bundle_dir(outdir):
238                                appimage=app_info['url'].split('/')[-1]
239                                appimage_url='/'.join(app_info['url'].split('/')[0:-1])
240                                self._debug("Fetching external appimage %s"%app_info['url'])
241                                applist=[appimage]
242                                self._debug("Processing info...")
243                                self._th_generate_xml_catalog(applist,outdir,app_info['url_info'],appimage_url,app_name,progress_bar)
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
249
250##              print (("Removing old entries..."))
251                self._clean_bundle_catalogue(all_apps,outdir)
252                return(True)
253        #def _download_bundles_catalogue
254
255        def _get_external_appimages(self):
256                external_appimages={}
257                if os.path.isfile(self.external_appimages):
258                        try:
259                                with open(self.external_appimages) as appimages:
260                                        external_appimages=json.load(appimages)
261                        except:
262                                self._debug("Can't load %s"%self.external_appimages)
263                self._debug(external_appimages)
264                return external_appimages
265
266        def _chk_bundle_dir(self,outdir):
267                msg_status=True
268                if not os.path.isdir(outdir):
269                        try:
270                                os.makedirs(outdir)
271                        except Exception as e:
272                                msg_status=False
273                                print(e)
274                return(os.access(outdir,os.W_OK|os.R_OK|os.X_OK|os.F_OK))
275        #def _chk_bundle_dir
276
277        def _fetch_repo(self,repo):
278                req=Request(repo, headers={'User-Agent':'Mozilla/5.0'})
279                with urllib.request.urlopen(req) as f:
280                        content=(f.read().decode('utf-8'))
281               
282                return(content)
283        #def _fetch_repo
284
285        def _generate_applist(self,content,repo_name):
286                garbage_list=[]
287                applist=[]
288                #webscrapping for probono repo
289                if repo_name=='probono':
290                        garbage_list=content.split(' ')
291                        for garbage_line in garbage_list:
292                                if garbage_line.endswith('AppImage"'):
293                                        app=garbage_line.replace('href=":','')
294                                        applist.append(app.replace('"',''))
295                #Example of a webscrapping from another site
296#               if repo_name='other_repo_name':
297#                       for garbage_line in garbage_list:
298#                                       if garbage_line.startswith('file="'):
299#                                               if 'appimage' in garbage_line:
300#                                                       app=garbage_line.replace('file="','')
301#                                                       app=app.replace('\n','')
302#                                                       self._debug("Add %s"%app)
303#                                                       applist.append(app.replace('"',''))
304
305                return(applist)
306        #def _generate_applist
307
308        def _th_generate_xml_catalog(self,applist,outdir,info_url,repo,repo_name,progress_bar=''):
309                CURSOR_UP='\033[F'
310                ERASE_LINE='\033[K'
311                maxconnections = 10
312                semaphore = threading.BoundedSemaphore(value=maxconnections)
313                random_applist = list(applist)
314                random.shuffle(random_applist)
315                len_applist=len(random_applist)
316                inc=30/len_applist
317#               print (CURSOR_UP)
318                for app in random_applist:
319                        th=threading.Thread(target=self._th_write_xml, args = (app,outdir,info_url,repo,repo_name,semaphore,inc))
320                        th.start()
321#               os.system('setterm -cursor off')
322#               while threading.active_count()>2: #Discard both main and own threads
323#                       for i in range(len(progress_bar),int(self.progress)):
324#                               progress_bar='#'+progress_bar
325#                       print (CURSOR_UP)
326#                       print (("Fetching %s catalogue: "+progress_bar)%repo_type,end="\r")
327#               os.system('setterm -cursor on')
328        #def _th_generate_xml_catalog
329
330        def _th_write_xml(self,app,outdir,info_url,repo,repo_name,semaphore,inc):
331                semaphore.acquire()
332                lock=threading.Lock()
333                name_splitted=app.split('-')
334                name=name_splitted[0]
335                if len(name_splitted)>1:
336                        version=name_splitted[1]
337                if len(name_splitted)>2:
338                        arch=name_splitted[2]
339                filename=outdir+app.lower().replace('appimage',"appdata.xml")
340                self._debug("checking if we need to download "+filename)
341                if not os.path.isfile(filename):
342                        self._write_xml_file(filename,app,name,version,info_url,repo,repo_name,lock)
343                with lock:
344                        self.progress=self.progress+inc
345                semaphore.release()
346        #def _th_write_xml
347
348        def _write_xml_file(self,filename,app,name,version,info_url,repo,repo_name,lock):
349                        self._debug("Generating "+app+" xml")
350                        f=open(filename,'w')
351                        f.write('<?xml version="1.0" encoding="UTF-8"?>'+"\n")
352                        f.write("<components version=\"0.10\">\n")
353                        f.write("<component  type=\"desktop-application\">\n")
354                        f.write("  <id>"+app.lower()+"</id>\n")
355                        f.write("  <pkgname>"+app+"</pkgname>\n")
356                        if version:
357                                version='-'+version
358                        f.write("  <name>"+name+version+"</name>\n")
359                        f.write("  <metadata_license>CC0-1.0</metadata_license>\n")
360                        f.write("  <provides><binary>"+app+"</binary></provides>\n")
361                        f.write("  <releases>\n")
362                        f.write("  <release version=\""+version+"\" timestamp=\"1408573857\"></release>\n")
363                        f.write("  </releases>\n")
364                        f.write("  <launchable type=\"desktop-id\">"+name+".desktop</launchable>\n")
365                        with lock:
366                                try:
367                                        if name in self.descriptions_dict.keys():
368                                                (description,icon)=self.descriptions_dict[name]
369                                        else:
370                                                (description,icon)=self._get_description_icon(name,info_url,repo_name)
371                                                self.descriptions_dict.update({name:[description,icon]})
372                                except:
373                                        description=''
374                                        icon=''
375                        summary=' '.join(list(description.split(' ')[:8]))
376                        description="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.\n"+description
377                        if not summary:
378                                summary=' '.join(list(description.split(' ')[:8]))
379                        f.write("  <description><p></p><p>"+description+"</p></description>\n")
380                        f.write("  <summary>"+summary+"...</summary>\n")
381                        f.write('  <url type="homepage">'+repo+'</url>\n')
382                        f.write("  <bundle type=\"appimage\">"+app+"</bundle>\n")
383                        f.write("  <keywords>\n")
384                        f.write("       <keyword>"+name+"</keyword>\n")
385                        f.write("       <keyword>appimage</keyword>\n")
386                        f.write("  </keywords>\n")
387                        f.write("  <categories>\n")
388                        f.write("       <category>AppImage</category>\n")
389#                       f.write("       <category>GTK</category>\n")
390                        f.write("  </categories>\n")
391#                       f.write("<icon type=\"cached\">"+name+"_"+name+".png</icon>\n")
392                        f.write("<icon type=\"local\">"+icon+"</icon>\n")
393                        f.write("</component>\n")
394                        f.write("</components>\n")
395                        f.close()
396        #def _write_xml_file
397
398        def _get_description_icon(self,app_name,info_url,repo_name):
399                desc=''
400                icon=''
401                if info_url:
402                        if '$(appname)' in info_url:
403                                info_url=info_url.replace('$(appname)',app_name)
404                        self._debug("Getting description from repo/app %s - %s "%(repo_name,info_url))
405                        try:
406                                with urllib.request.urlopen(info_url) as f:
407                                        if repo_name=='probono':
408                                                content=(f.read().decode('utf-8'))
409                                                soup=BeautifulSoup(content,"html.parser")
410                                                description_div=soup.findAll('div', attrs={ "class" : "description-text"})
411                                                icon_div=soup.findAll('div', attrs={ "class" : "avatar-icon avatar-large description-icon "})
412                                if len(description_div)>0:
413                                        desc=description_div[0].text
414                                        desc=desc.replace(':','.')
415                                        desc=desc.replace('&','&amp;')
416                                if len(icon_div)>0:
417                                        icon_str=str(icon_div[0])
418                                        icon=icon_str.split(' ')[9]
419                                        icon=icon.lstrip('url(')
420                                        if icon.startswith('http'):
421                                                icon=icon.rstrip(');"></div>')
422                                                icon=self._download_file(icon,app_name)
423                                        print("Icon: %s"%icon)
424                        except Exception as e:
425                                print("Can't get description from "+info_url)
426                                print(str(e))
427                                pass
428                return([desc,icon])
429        #def _get_description
430
431        def _clean_bundle_catalogue(self,applist,outdir):
432                xml_files_list=[]
433                applist=[item.lower() for item in applist]
434                for xml_file in os.listdir(outdir):
435                        if xml_file.endswith('appdata.xml'):
436                                xml_files_list.append(xml_file.lower().replace('appdata.xml','appimage'))
437       
438                if xml_files_list:
439                        xml_discard_list=list(set(xml_files_list).difference(applist))
440                        for discarded_file in xml_discard_list:
441                                os.remove(outdir+'/'+discarded_file.replace('appimage','appdata.xml'))
442        #def _clean_bunlde_catalogue
443
444        def _download_file(self,url,app_name):
445#               target_file=self.icons_folder+'/'+app_name+".png"
446                target_file=self.icons_dir+'/'+app_name+".png"
447                if not os.path.isfile(target_file):
448#                       shutil.copy("/usr/share/icons/hicolor/128x128/apps/lliurex-store.png",target_file)
449#                       if not os.fork():
450                        if not os.path.isfile(target_file):
451                                self._debug("Downloading %s to %s"%(url,target_file))
452                                try:
453                                        with urllib.request.urlopen(url) as response, open(target_file, 'wb') as out_file:
454                                                bf=16*1024
455                                                acumbf=0
456                                                file_size=int(response.info()['Content-Length'])
457                                                while True:
458                                                        if acumbf>=file_size:
459                                                            break
460                                                        shutil.copyfileobj(response, out_file,bf)
461                                                        acumbf=acumbf+bf
462                                        st = os.stat(target_file)
463                                except Exception as e:
464                                        self._debug("Unable to download %s"%url)
465                                        self._debug("Reason: %s"%e)
466                                        target_file=url
467#                               os._exit(0)
468                return(target_file)
469        #def _download_file
Note: See TracBrowser for help on using the repository browser.