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

Last change on this file since 8223 was 8223, checked in by Juanma, 13 months ago

Implemented cache

File size: 12.6 KB
Line 
1#The name of the main class must match the file name in lowercase
2import os
3import urllib
4import shutil
5import gi
6from gi.repository import Gio
7gi.require_version ('Snapd', '1')
8from gi.repository import Snapd
9gi.require_version('AppStreamGlib', '1.0')
10from gi.repository import AppStreamGlib as appstream
11import time
12import html
13import threading
14from queue import Queue as pool
15#Needed for async find method, perhaps only on xenial
16wrap=Gio.SimpleAsyncResult()
17class snapmanager:
18       
19        def __init__(self):
20                self.dbg=False
21                self.progress=0
22                self.partial_progress=0
23                self.plugin_actions={'install':'snap','remove':'snap','pkginfo':'snap','load':'snap'}
24                self.cache_dir=os.getenv("HOME")+"/.cache/lliurex-store"
25                self.cache_xmls=self.cache_dir+'/xmls/snap'
26                self.cache_last_update=self.cache_xmls+'/.snap.lu'
27                self.icons_folder=self.cache_dir+"/icons"
28                self.images_folder=self.cache_dir+"/images"
29                self.result={}
30                self.result['data']={}
31                self.result['status']={}
32                self.disabled=False
33                self.icon_cache_enabled=True
34                self.image_cache_enabled=True
35                self.cli_mode=False
36                if not os.path.isdir(self.icons_folder):
37                        try:
38                                os.makedirs(self.icons_folder)
39                        except:
40                                self.icon_cache_enabled=False
41                                #self._debug("Icon cache disabled")
42                if not os.path.isdir(self.images_folder):
43                        try:
44                                os.makedirs(self.images_folder)
45                        except:
46                                self.image_cache_enabled=False
47                                #self._debug("Image cache disabled")
48        #def __init__
49
50        def set_debug(self,dbg=True):
51                self.dbg=dbg
52                #self._debug ("Debug enabled")
53        #def set_debug
54
55        def _debug(self,msg=''):
56                if self.dbg:
57                        print ('DEBUG snap: %s'%msg)
58        #def debug
59
60        def register(self):
61                return(self.plugin_actions)
62        #def register
63
64        def enable(self,state=False):
65                self.disabled=state
66        #def enable
67
68        def execute_action(self,action,applist=None,store=None,results=0):
69                if store:
70                        self.store=store
71                else:
72                        self.store=appstream.Store()
73                self.progress=0
74                self.result['status']={'status':-1,'msg':''}
75                self.result['data']=''
76               
77                self.snap_client=Snapd.Client()
78                try:
79                        self.snap_client.connect_sync(None)
80                except Exception as e:
81                        self.disabled=True
82                        #self._debug("Disabling snap %s"%e)
83
84                if self.disabled==True:
85                        self._set_status(9)
86                        self.result['data']=self.store
87                else:
88                        self._check_dirs()
89                        dataList=[]
90                        if action=='load':
91                                self.result['data']=self._load_snap_store(self.store)
92                        else:
93                                for app_info in applist:
94                                        self.partial_progress=0
95                                        if action=='install':
96                                                dataList.append(self._install_snap(app_info))
97                                        if action=='remove':
98                                                dataList.append(self._remove_snap(app_info))
99                                        if action=='pkginfo':
100                                                dataList.append(self._get_info(app_info))
101                                        self.progress+=round(self.partial_progress/len(applist),1)
102                                        if self.progress>98:
103                                                self.progress=98
104                                self.result['data']=list(dataList)
105                self.progress=100
106                return(self.result)
107        #def execute_action
108
109        def _set_status(self,status,msg=''):
110                self.result['status']={'status':status,'msg':msg}
111        #def _set_status
112
113        def _callback(self,client,change, _,user_data):
114            # Interate over tasks to determine the aggregate tasks for completion.
115            total = 0
116            done = 0
117            for task in change.get_tasks():
118                total += task.get_progress_total()
119                done += task.get_progress_done()
120            self.progress = round((done/total)*100)
121        #def _callback
122
123        def _check_dirs(self):
124                if not os.path.isdir(self.cache_xmls):
125                        os.makedirs(self.cache_xmls)
126        #def _check_dirs
127
128        def _load_snap_store(self,store):
129                pkgs=[]
130                #Look if cache is up-to-date
131                if os.path.isfile(self.cache_last_update):
132                        epoch_time=time.time()
133                        fcache=open(self.cache_last_update,'r')
134                        fcache_update=fcache.read()
135                        if not fcache_update:
136                                fcache_update=0
137                        if int(epoch_time)-int(fcache_update)<86400:
138                                if os.listdir(os.path.dirname(self.cache_xmls)):
139                                        #self._debug("Loading snap from cache")
140                                        store=self._load_from_cache(store)
141                                        return store
142
143                fcache=open(self.cache_last_update,'w')
144                fcache.write(str(int(time.time())))
145                pkgs=self._load_sections()
146                self._set_status(1)
147                store_pool=pool()
148                for pkg in pkgs:
149                        maxconnections = 10
150                        threads=[]
151                        semaphore = threading.BoundedSemaphore(value=maxconnections)
152                        th=threading.Thread(target=self._th_load_store, args = (store_pool,pkg,semaphore))
153                        threads.append(th)
154                        th.start()
155                for thread in threads:
156                        try:
157                                thread.join()
158                        except:
159                                pass
160                while store_pool.qsize():
161                        store.add_app(store_pool.get())
162                return(store)
163        #def _load_snap_store
164
165        def _th_load_store(self,store,pkg,semaphore):
166                semaphore.acquire()
167                app=self.store.get_app_by_pkgname(pkg.get_name())
168                if not app:
169                        #self._debug("Searching for %s"%pkg.get_name())
170                        app=self.store.get_app_by_id(pkg.get_name().lower()+".desktop")
171                        if app:
172                                bundle=appstream.Bundle()
173                                bundle.set_kind(bundle.kind_from_string('SNAP'))
174                                bundle.set_id(pkg.get_name()+'.snap')
175                                app.add_bundle(bundle)
176                                app.add_category("Snap")
177                                store.put(self._generate_appstream_app_from_snap(pkg))
178                        else:
179                                store.put(self._generate_appstream_app_from_snap(pkg))
180                semaphore.release()
181        #def _th_load_store
182
183        def _load_from_cache(self,store):
184                for target_file in os.listdir(self.cache_xmls):
185                        if target_file.endswith('.xml'):
186                                store_file=Gio.File.new_for_path(self.cache_xmls+'/'+target_file)
187                                #self._debug("Adding file %s/%s"%(self.cache_xmls,target_file))
188                                try:
189                                        store.from_file(store_file,'',None)
190                                except Exception as e:
191                                        #self._debug("Couldn't add file %s to store"%target_file)
192                                        #self._debug("Reason: %s"%e)
193                                        pass
194                return store   
195        #def _load_from_cache
196
197        def _generate_appstream_app_from_snap(self,pkg):
198                bundle=appstream.Bundle()
199                app=appstream.App()
200                icon=appstream.Icon()
201                screenshot=appstream.Screenshot()
202#               bundle.set_kind(appstream.BundleKind.SNAP)
203                bundle.set_kind(bundle.kind_from_string('SNAP'))
204                bundle.set_id(pkg.get_name()+'.snap')
205                app.add_bundle(bundle)
206                app.set_name("C",pkg.get_name()+'.snap')
207                app.add_pkgname(pkg.get_name()+'.snap')
208                app.add_category("Snap")
209                release=appstream.Release()
210                release.set_version(pkg.get_version())
211                app.add_release(release)
212                app.set_id("io.snapcraft.%s"%pkg.get_name()+'.snap')
213                app.set_id_kind=appstream.IdKind.DESKTOP
214                app.set_metadata_license("CC0-1.0")
215                description="This is an Snap bundle. It hasn't been tested by our developers and comes from a 3rd party dev team. Please use it carefully."
216                pkg_description=pkg.get_description()
217                pkg_description=html.escape(pkg_description,quote=True)
218                pkg_description=pkg_description.replace("<","&gt;")
219                app.set_description("C","<p>%s</p><p>%s</p>"%(description,pkg_description))
220                app.set_comment("C",pkg.get_summary().rstrip('.'))
221
222                app.add_keyword("C",pkg.get_name())
223                for word in pkg.get_summary().split(' '):
224                        if len(word)>3:
225                                app.add_keyword("C",word)
226
227                if pkg.get_icon():
228                        if self.icon_cache_enabled:
229                                icon.set_kind(appstream.IconKind.LOCAL)
230                                icon.set_filename(self._download_file(pkg.get_icon(),pkg.get_name(),self.icons_folder))
231                        else:
232                                icon.set_kind(appstream.IconKind.REMOTE)
233                                icon.set_name(pkg.get_icon())
234                                icon.set_url(pkg.get_icon())
235                        app.add_icon(icon)
236
237                if pkg.get_license():
238                        app.set_project_license(pkg.get_license())
239
240                if pkg.get_screenshots():
241                        for snap_screen in pkg.get_screenshots():
242                                img=appstream.Image()
243                                img.set_kind(appstream.ImageKind.SOURCE)
244                                img.set_url(snap_screen.get_url())
245                                break
246                        screenshot.add_image(img)
247                        app.add_screenshot(screenshot)
248
249                if not os.path.isfile(self.cache_xmls+'/'+app.get_id_filename()):
250                        xml_path='%s/%s.xml'%(self.cache_xmls,app.get_id_filename())
251                        gioFile=Gio.File.new_for_path(xml_path)
252                        app.to_file(gioFile)
253                        #Fix some things in app_file...
254                        xml_file=open(xml_path,'r',encoding='utf-8')
255                        xml_data=xml_file.readlines()
256                        xml_file.close()
257                        #self._debug("fixing %s"%xml_path)
258                        try:
259                                xml_data[0]=xml_data[0]+"<components>\n"
260                                xml_data[-1]=xml_data[-1]+"\n"+"</components>"
261                        except:
262                                pass
263                        xml_file=open(xml_path,'w')
264                        xml_file.writelines(xml_data)
265                        xml_file.close()
266                return(app)
267        #def _generate_appstream_app_from_snap
268
269        def _search_cb(self,obj,request,*args):
270                global wrap
271                wrap=request
272        #def _search_cb
273
274        def _load_sections(self):
275                sections=self.snap_client.get_sections_sync()
276                stable_pkgs=[]
277                for section in sections:
278                        apps=self.snap_client.find_section_sync(Snapd.FindFlags.MATCH_NAME,section,None)
279                        for pkg in apps:
280                                stable_pkgs.append(pkg)
281                return(stable_pkgs)
282        #def _load_sections
283
284        def _search_snap_async(self,tokens,force_stable=True):
285                #self._debug("Async Searching %s"%tokens)
286                pkgs=None
287                global wrap
288                self.snap_client.find_async(Snapd.FindFlags.MATCH_NAME,
289                                                        tokens,
290                                                        None,
291                                                        self._search_cb,(None,),None)
292                while 'Snapd' not in str(type(wrap)):
293                        time.sleep(0.1)
294                snaps=self.snap_client.find_finish(wrap)
295                if type(snaps)!=type([]):
296                        pkgs=[snaps]
297                else:
298                        pkgs=snaps
299                stable_pkgs=[]
300                for pkg in pkgs:
301                        if force_stable:
302                                if pkg.get_channel()=='stable':
303                                        stable_pkgs.append(pkg)
304                                else:
305                                        #self._debug(pkg.get_channel())
306                                        pass
307                        else:
308                                stable_pkgs.append(pkg)
309                return(stable_pkgs)
310        #def _search_snap_async
311
312        def _search_snap(self,tokens,force_stable=True):
313                #self._debug("Searching %s"%tokens)
314                pkg=None
315                pkgs=None
316                try:
317                        pkgs=self.snap_client.find_sync(Snapd.FindFlags.MATCH_NAME,tokens,None)
318                except Exception as e:
319                        print("ERR: %s"%e)
320                        self._set_status(1)
321                stable_pkgs=[]
322                for pkg in pkgs:
323                        if force_stable:
324                                if pkg.get_channel()=='stable':
325                                        stable_pkgs.append(pkg)
326                                else:
327                                        #self._debug(pkg.get_channel())
328                                        pass
329                        else:
330                                stable_pkgs.append(pkg)
331                #self._debug("Done")
332                return(stable_pkgs)
333        #def _search_snap
334
335        def _download_file(self,url,app_name,dest_dir):
336                target_file=dest_dir+'/'+app_name+".png"
337                if not os.path.isfile(target_file):
338                        if not os.path.isfile(target_file):
339                                #self._debug("Downloading %s to %s"%(url,target_file))
340                                try:
341                                        with urllib.request.urlopen(url) as response, open(target_file, 'wb') as out_file:
342                                                bf=16*1024
343                                                acumbf=0
344                                                file_size=int(response.info()['Content-Length'])
345                                                while True:
346                                                        if acumbf>=file_size:
347                                                            break
348                                                        shutil.copyfileobj(response, out_file,bf)
349                                                        acumbf=acumbf+bf
350                                        st = os.stat(target_file)
351                                except Exception as e:
352                                        #self._debug("Unable to download %s"%url)
353                                        #self._debug("Reason: %s"%e)
354                                        target_file=''
355                return(target_file)
356        #def _download_file
357
358
359        def _get_info(self,app_info):
360                #switch to launch async method when running under a gui
361                #Request will block when in sync mode under a gui and async mode blocks when on cli (really funny)
362                #self._debug("Getting info for %s"%app_info)
363                pkg=None
364                try:
365                        pkg=self.snap_client.list_one_sync(app_info['package'].replace('.snap',''))
366                        app_info['state']='installed'
367                        pkgs=[pkg]
368                except:
369                        app_info['state']='available'
370                if not app_info['size']:
371                        if self.cli_mode:
372                                pkgs=self._search_snap(app_info['package'].replace('.snap',''),force_stable=False)
373                        else:
374                                pkgs=self._search_snap_async(app_info['package'].replace('.snap',''),force_stable=False)
375                        #self._debug("Getting extended info for %s %s"%(app_info['package'],pkgs))
376                        if type(pkgs)==type([]):
377                                for pkg in pkgs:
378                                        #self._debug("Getting extended info for %s"%app_info['name'])
379                                        if pkg.get_download_size():
380                                                app_info['size']=str(pkg.get_download_size())
381                                        elif pkg.get_installed_size():
382                                                app_info['size']=str(pkg.get_installed_size())
383                                        else:
384                                                app_info['size']="-1"
385                                        break
386                        else:
387                                app_info['size']='0'
388                #self._debug("Info for %s"%app_info)
389                self.partial_progress=100
390                return(app_info)
391        #def _get_info
392
393        def _install_snap(self,app_info):
394                #self._debug("Installing %s"%app_info['name'])
395                def install(app_name,flags):
396                        self.snap_client.install2_sync(flags,app_name.replace('.snap',''),
397                                        None, # channel
398                                        None, #revision
399                                        self._callback, (None,),
400                                        None) # cancellable
401                        app_info['state']='installed'
402                        self._set_status(0)
403
404                if app_info['state']=='installed':
405                        self._set_status(4)
406                else:
407                        try:
408                                install(app_info['name'],Snapd.InstallFlags.NONE)
409                        except Exception as e:
410                                try:
411                                        if e.code==19:
412                                                install(app_info['name'],Snapd.InstallFlags.CLASSIC)
413                                except Exception as e:
414                                        #self._debug("Install error %s"%e)
415                                        self._set_status(5)
416                #self._debug("Installed %s"%app_info)
417                return app_info
418        #def _install_snap
419
420        def _remove_snap(self,app_info):
421                if app_info['state']=='available':
422                        self._set_status(3)
423                else:
424                        try:
425                                self.snap_client.remove_sync(app_info['name'].replace('.snap',''),
426                       self._callback, (None,),
427                                                None) # cancellable
428                                app_info['state']='available'
429                                self._set_status(0)
430                        except Exception as e:
431                                print("Remove error %s"%e)
432                                self._set_status(6)
433                return app_info
434        #def _remove_snap
Note: See TracBrowser for help on using the repository browser.