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

Last change on this file since 7079 was 7079, checked in by Juanma, 20 months ago

External appimages file

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