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

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

Fixed snap channels

File size: 14.2 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.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={'synfig64':{'url':'https://kent.dl.sourceforge.net/project/synfig/releases/1.2.1/linux/SynfigStudio-1.2.1-64bit.appimage','url_info':''},'synfig32':{'url':'https://kent.dl.sourceforge.net/project/synfig/releases/1.2.1/linux/SynfigStudio-1.2.1-32bit.appimage','url_info':''}}
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: '+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.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 _chk_bundle_dir(self,outdir):
255                msg_status=True
256                if not os.path.isdir(outdir):
257                        try:
258                                os.makedirs(outdir)
259                        except Exception as e:
260                                msg_status=False
261                                print(e)
262                return(os.access(outdir,os.W_OK|os.R_OK|os.X_OK|os.F_OK))
263        #def _chk_bundle_dir
264
265        def _fetch_repo(self,repo):
266                req=Request(repo, headers={'User-Agent':'Mozilla/5.0'})
267                with urllib.request.urlopen(req) as f:
268                        content=(f.read().decode('utf-8'))
269               
270                return(content)
271        #def _fetch_repo
272
273        def _generate_applist(self,content,repo_name):
274                garbage_list=[]
275                applist=[]
276                #webscrapping for probono repo
277                if repo_name=='probono':
278                        garbage_list=content.split(' ')
279                        for garbage_line in garbage_list:
280                                if garbage_line.endswith('AppImage"'):
281                                        app=garbage_line.replace('href=":','')
282                                        applist.append(app.replace('"',''))
283                #Example of a webscrapping from another site
284#               if repo_name='other_repo_name':
285#                       for garbage_line in garbage_list:
286#                                       if garbage_line.startswith('file="'):
287#                                               if 'appimage' in garbage_line:
288#                                                       app=garbage_line.replace('file="','')
289#                                                       app=app.replace('\n','')
290#                                                       self._debug("Add %s"%app)
291#                                                       applist.append(app.replace('"',''))
292
293                return(applist)
294        #def _generate_applist
295
296        def _th_generate_xml_catalog(self,applist,outdir,info_url,repo,repo_name,progress_bar=''):
297                CURSOR_UP='\033[F'
298                ERASE_LINE='\033[K'
299                maxconnections = 10
300                semaphore = threading.BoundedSemaphore(value=maxconnections)
301                random_applist = list(applist)
302                random.shuffle(random_applist)
303                len_applist=len(random_applist)
304                inc=30/len_applist
305#               print (CURSOR_UP)
306                for app in random_applist:
307                        th=threading.Thread(target=self._th_write_xml, args = (app,outdir,info_url,repo,repo_name,semaphore,inc))
308                        th.start()
309#               os.system('setterm -cursor off')
310                while threading.active_count()>2: #Discard both main and own threads
311                        for i in range(len(progress_bar),int(self.progress)):
312                                progress_bar='#'+progress_bar
313#                       print (CURSOR_UP)
314#                       print (("Fetching %s catalogue: "+progress_bar)%repo_type,end="\r")
315#               os.system('setterm -cursor on')
316        #def _th_generate_xml_catalog
317
318        def _th_write_xml(self,app,outdir,info_url,repo,repo_name,semaphore,inc):
319                semaphore.acquire()
320                lock=threading.Lock()
321                name_splitted=app.split('-')
322                name=name_splitted[0]
323                version=name_splitted[1]
324                arch=name_splitted[2]
325                filename=outdir+app.lower().replace('appimage',"appdata.xml")
326                self._debug("checking if we need to download "+filename)
327                if not os.path.isfile(filename):
328                        self._write_xml_file(filename,app,name,version,info_url,repo,repo_name,lock)
329                with lock:
330                        self.progress=self.progress+inc
331                semaphore.release()
332        #def _th_write_xml
333
334        def _write_xml_file(self,filename,app,name,version,info_url,repo,repo_name,lock):
335                        self._debug("Generating "+app+" xml")
336                        f=open(filename,'w')
337                        f.write('<?xml version="1.0" encoding="UTF-8"?>'+"\n")
338                        f.write("<components version=\"0.10\">\n")
339                        f.write("<component  type=\"desktop-application\">\n")
340                        f.write("  <id>"+app.lower()+"</id>\n")
341                        f.write("  <pkgname>"+app+"</pkgname>\n")
342                        f.write("  <name>"+name+"</name>\n")
343                        f.write("  <metadata_license>CC0-1.0</metadata_license>\n")
344                        f.write("  <provides><binary>"+app+"</binary></provides>\n")
345                        f.write("  <releases>\n")
346                        f.write("  <release version=\""+version+"\" timestamp=\"1408573857\"></release>\n")
347                        f.write("  </releases>\n")
348                        f.write("  <launchable type=\"desktop-id\">"+name+".desktop</launchable>\n")
349                        with lock:
350                                try:
351                                        if name in self.descriptions_dict.keys():
352                                                description=self.descriptions_dict[name]
353                                        else:
354                                                description=self._get_description(name,info_url,repo_name)
355                                                self.descriptions_dict.update({name:description})
356                                except:
357                                        description=''
358                        summary=' '.join(list(description.split(' ')[:8]))
359                        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
360                        if not summary:
361                                summary=' '.join(list(description.split(' ')[:8]))
362                        f.write("  <description><p></p><p>"+description+"</p></description>\n")
363                        f.write("  <summary>"+summary+"...</summary>\n")
364                        f.write('  <url type="homepage">'+repo+'</url>\n')
365                        f.write("  <bundle type=\"appimage\">"+app+"</bundle>\n")
366                        f.write("  <keywords>\n")
367                        f.write("       <keyword>"+name+"</keyword>\n")
368                        f.write("       <keyword>appimage</keyword>\n")
369                        f.write("  </keywords>\n")
370                        f.write("  <categories>\n")
371                        f.write("       <category>AppImage</category>\n")
372#                       f.write("       <category>GTK</category>\n")
373                        f.write("  </categories>\n")
374                        f.write("<icon type=\"cached\">"+name+"_"+name+".png</icon>\n")
375                        f.write("</component>\n")
376                        f.write("</components>\n")
377                        f.close()
378        #def _write_xml_file
379
380        def _get_description(self,app_name,info_url,repo_name):
381                desc=''
382                if '$(appname)' in info_url:
383                        info_url=info_url.replace('$(appname)',app_name)
384                if info_url:
385                        self._debug("Getting description from repo/app %s - %s "%(repo_name,info_url))
386                        try:
387                                with urllib.request.urlopen(info_url) as f:
388                                        if repo_name=='probono':
389                                                content=(f.read().decode('utf-8'))
390                                                soup=BeautifulSoup(content,"html.parser")
391                                                description_div=soup.findAll('div', attrs={ "class" : "description-text"})
392                                if len(description_div)>0:
393                                        desc=description_div[0].text
394                                        desc=desc.replace(':','.')
395                                        desc=desc.replace('&','&amp;')
396                        except Exception as e:
397                                print("Can't get description from "+info_url)
398                                print(str(e))
399                                pass
400                return(desc)
401        #def _get_description
402
403        def _clean_bundle_catalogue(self,applist,outdir):
404                xml_files_list=[]
405                applist=[item.lower() for item in applist]
406                for xml_file in os.listdir(outdir):
407                        if xml_file.endswith('appdata.xml'):
408                                xml_files_list.append(xml_file.lower().replace('appdata.xml','appimage'))
409       
410                if xml_files_list:
411                        xml_discard_list=list(set(xml_files_list).difference(applist))
412                        for discarded_file in xml_discard_list:
413                                os.remove(outdir+'/'+discarded_file.replace('appimage','appdata.xml'))
414        #def _clean_bunlde_catalogue
415
Note: See TracBrowser for help on using the repository browser.