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

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

Disabled debug mode in appimageManager

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