source: lliurex-store-appimage/trunk/fuentes/.pybuild/pythonX.Y_3.5/build/lliurexstore/plugins/appImageManager.py @ 6733

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

Appimage plugin for the store

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