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

Last change on this file since 7092 was 7092, checked in by Juanma, 3 years ago

Gui supports full path for local icons * Added icons to appimages and snaps

File size: 15.8 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                version=name_splitted[1]
336                arch=name_splitted[2]
337                filename=outdir+app.lower().replace('appimage',"appdata.xml")
338                self._debug("checking if we need to download "+filename)
339                if not os.path.isfile(filename):
340                        self._write_xml_file(filename,app,name,version,info_url,repo,repo_name,lock)
341                with lock:
342                        self.progress=self.progress+inc
343                semaphore.release()
344        #def _th_write_xml
345
346        def _write_xml_file(self,filename,app,name,version,info_url,repo,repo_name,lock):
347                        self._debug("Generating "+app+" xml")
348                        f=open(filename,'w')
349                        f.write('<?xml version="1.0" encoding="UTF-8"?>'+"\n")
350                        f.write("<components version=\"0.10\">\n")
351                        f.write("<component  type=\"desktop-application\">\n")
352                        f.write("  <id>"+app.lower()+"</id>\n")
353                        f.write("  <pkgname>"+app+"</pkgname>\n")
354                        f.write("  <name>"+name+"</name>\n")
355                        f.write("  <metadata_license>CC0-1.0</metadata_license>\n")
356                        f.write("  <provides><binary>"+app+"</binary></provides>\n")
357                        f.write("  <releases>\n")
358                        f.write("  <release version=\""+version+"\" timestamp=\"1408573857\"></release>\n")
359                        f.write("  </releases>\n")
360                        f.write("  <launchable type=\"desktop-id\">"+name+".desktop</launchable>\n")
361                        with lock:
362                                try:
363                                        if name in self.descriptions_dict.keys():
364                                                (description,icon)=self.descriptions_dict[name]
365                                        else:
366                                                (description,icon)=self._get_description_icon(name,info_url,repo_name)
367                                                self.descriptions_dict.update({name:[description,icon]})
368                                except:
369                                        description=''
370                                        icon=''
371                        summary=' '.join(list(description.split(' ')[:8]))
372                        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
373                        if not summary:
374                                summary=' '.join(list(description.split(' ')[:8]))
375                        f.write("  <description><p></p><p>"+description+"</p></description>\n")
376                        f.write("  <summary>"+summary+"...</summary>\n")
377                        f.write('  <url type="homepage">'+repo+'</url>\n')
378                        f.write("  <bundle type=\"appimage\">"+app+"</bundle>\n")
379                        f.write("  <keywords>\n")
380                        f.write("       <keyword>"+name+"</keyword>\n")
381                        f.write("       <keyword>appimage</keyword>\n")
382                        f.write("  </keywords>\n")
383                        f.write("  <categories>\n")
384                        f.write("       <category>AppImage</category>\n")
385#                       f.write("       <category>GTK</category>\n")
386                        f.write("  </categories>\n")
387#                       f.write("<icon type=\"cached\">"+name+"_"+name+".png</icon>\n")
388                        f.write("<icon type=\"local\">"+icon+"</icon>\n")
389                        f.write("</component>\n")
390                        f.write("</components>\n")
391                        f.close()
392        #def _write_xml_file
393
394        def _get_description_icon(self,app_name,info_url,repo_name):
395                desc=''
396                icon=''
397                if info_url:
398                        if '$(appname)' in info_url:
399                                info_url=info_url.replace('$(appname)',app_name)
400                        self._debug("Getting description from repo/app %s - %s "%(repo_name,info_url))
401                        try:
402                                with urllib.request.urlopen(info_url) as f:
403                                        if repo_name=='probono':
404                                                content=(f.read().decode('utf-8'))
405                                                soup=BeautifulSoup(content,"html.parser")
406                                                description_div=soup.findAll('div', attrs={ "class" : "description-text"})
407                                                icon_div=soup.findAll('div', attrs={ "class" : "avatar-icon avatar-large description-icon "})
408                                if len(description_div)>0:
409                                        desc=description_div[0].text
410                                        desc=desc.replace(':','.')
411                                        desc=desc.replace('&','&amp;')
412                                if len(icon_div)>0:
413                                        icon_str=str(icon_div[0])
414                                        icon=icon_str.split(' ')[9]
415                                        icon=icon.lstrip('url(')
416                                        if icon.startswith('http'):
417                                                icon=icon.rstrip(');"></div>')
418                                                icon=self._download_file(icon,app_name)
419                                        print("Icon: %s"%icon)
420                        except Exception as e:
421                                print("Can't get description from "+info_url)
422                                print(str(e))
423                                pass
424                return([desc,icon])
425        #def _get_description
426
427        def _clean_bundle_catalogue(self,applist,outdir):
428                xml_files_list=[]
429                applist=[item.lower() for item in applist]
430                for xml_file in os.listdir(outdir):
431                        if xml_file.endswith('appdata.xml'):
432                                xml_files_list.append(xml_file.lower().replace('appdata.xml','appimage'))
433       
434                if xml_files_list:
435                        xml_discard_list=list(set(xml_files_list).difference(applist))
436                        for discarded_file in xml_discard_list:
437                                os.remove(outdir+'/'+discarded_file.replace('appimage','appdata.xml'))
438        #def _clean_bunlde_catalogue
439
440        def _download_file(self,url,app_name):
441#               target_file=self.icons_folder+'/'+app_name+".png"
442                target_file=self.icons_dir+'/'+app_name+".png"
443                if not os.path.isfile(target_file):
444#                       shutil.copy("/usr/share/icons/hicolor/128x128/apps/lliurex-store.png",target_file)
445#                       if not os.fork():
446                        if not os.path.isfile(target_file):
447                                self._debug("Downloading %s to %s"%(url,target_file))
448                                try:
449                                        with urllib.request.urlopen(url) as response, open(target_file, 'wb') as out_file:
450                                                bf=16*1024
451                                                acumbf=0
452                                                file_size=int(response.info()['Content-Length'])
453                                                while True:
454                                                        if acumbf>=file_size:
455                                                            break
456                                                        shutil.copyfileobj(response, out_file,bf)
457                                                        acumbf=acumbf+bf
458                                        st = os.stat(target_file)
459                                except Exception as e:
460                                        self._debug("Unable to download %s"%url)
461                                        self._debug("Reason: %s"%e)
462                                        target_file=url
463#                               os._exit(0)
464                return(target_file)
465        #def _download_file
Note: See TracBrowser for help on using the repository browser.