1+
2+ # Pckgd
3+ # Title: Pckgd
4+ # Description: A module for managing packages and their updates.
5+ # Updates from: github/TenthPres/TouchPointScripts/Pckgd/Pckgd.py
6+ # Version: X
7+ # License: AGPL-3.0
8+ # Author: James Kurtz
9+
10+ # Do not make edits to these files. They will be overwritten during updates.
11+
12+ global model , q , Data
13+ import json
14+
15+
16+ class Pckgd :
17+ def __init__ (self , type_id , name , body ):
18+ self .filename = name
19+ self .body = body
20+ self .typeId = type_id
21+ self .headers = {}
22+ self .version = None
23+ self .parse_headers ()
24+ self .determine_version ()
25+
26+ do_not_edit_demarcation = "========="
27+
28+ @staticmethod
29+ def find_installed_packages ():
30+ return [Pckgd (p .TypeId , p .Name , p .Body ) for p in Pckgd .find_installed_files ()]
31+
32+
33+ """ Finds all installed package contents in the system. """
34+ @staticmethod
35+ def find_installed_files ():
36+ # noinspection SqlResolve
37+ return q .QuerySql ("""
38+ SELECT Name, Body, TypeId, * FROM Content c
39+ WHERE (c.TypeId = 5 AND (
40+ c.Body LIKE '%' + CHAR(10) + '# Pckgd%' OR
41+ c.Body LIKE '# Pckgd%'))
42+ OR (c.Body LIKE '%-- Pckgd%' AND c.TypeId = 4);
43+ """ )
44+
45+ """ Parses the headers from the body of the content. """
46+ def parse_headers (self ):
47+ lines = self .body .splitlines ()
48+ for line in lines :
49+ if (self .typeId == 5 and line .strip ().startswith ('#' )) or \
50+ (self .typeId == 4 and line .strip ().startswith ('--' )):
51+ parts = line [1 :].split (':' , 1 )
52+ if len (parts ) == 2 :
53+ key = parts [0 ].strip ()
54+ value = parts [1 ].strip ()
55+
56+ # standardize casing of key
57+ key = ' ' .join (word .capitalize () for word in key .split ())
58+
59+ # Only process headers not seen yet
60+ if key not in self .headers :
61+ self .headers [key ] = value
62+ elif Pckgd .do_not_edit_demarcation in line :
63+ break
64+ return
65+
66+ def determine_version (self ):
67+ # if version header is set AND it's a hex value or numbered, assume it's right.
68+ if 'Version' in self .headers and self .headers ['Version' ].strip () != "" and all (c in '0123456789abcdefABCDEF.' for c in self .headers ['Version' ]):
69+ self .version = self .headers ['Version' ]
70+ else :
71+ self .version = Pckgd .calculate_version_hash (self .body )
72+ self .headers ['Version' ] = self .version
73+
74+ """ Sets or updates a header for a given body. """
75+ @staticmethod
76+ def set_header (body , key , value , type_id ):
77+ lines = body .splitlines ()
78+ new_lines = []
79+ header_found = False
80+ demarc_found = False
81+ for line in lines :
82+ if (type_id == 5 and line .strip ().startswith ('#' )) or \
83+ (type_id == 4 and line .strip ().startswith ('--' )):
84+ parts = line [1 :].split (':' , 1 )
85+ if len (parts ) == 2 and parts [0 ].strip () == key and not header_found and not demarc_found :
86+ new_lines .append ("{} {}: {}" .format (('#' if type_id == 5 else '--' ), key , value ))
87+ header_found = True
88+ else :
89+ new_lines .append (line )
90+ elif Pckgd .do_not_edit_demarcation in line :
91+ demarc_found = True # once we hit the demarcation, stop looking for headers
92+
93+ if not header_found : # Doesn't exist, so needs to be inserted.
94+ insert_index = 0
95+ pckgd_found = False
96+ for i , line in enumerate (new_lines ):
97+ if "Pckgd" in line :
98+ insert_index = i + 1 # keep iterating until we get to the Pckgd header
99+ pckgd_found = True
100+ elif not pckgd_found :
101+ insert_index = i + 1
102+ elif (type_id == 5 and line .strip ().startswith ('#' )) or \
103+ (type_id == 4 and line .strip ().startswith ('--' )):
104+ insert_index = i + 1
105+ elif pckgd_found :
106+ break
107+ elif Pckgd .do_not_edit_demarcation in line :
108+ insert_index = - 1
109+
110+ if insert_index > - 1 :
111+ new_lines .insert (insert_index , "{} {}: {}" .format (('#' if type_id == 5 else '--' ), key , value ))
112+
113+ return '\n ' .join (new_lines )
114+
115+ """ Calculates a version hash for a given piece of content. This is compared to the version header. """
116+ @staticmethod
117+ def calculate_version_hash (content_body ):
118+ import hashlib
119+ hash_object = hashlib .sha256 (content_body .strip ().encode ('utf-8' ))
120+ return hash_object .hexdigest ()[:8 ] # return first 8 characters of the hash
121+
122+
123+ def get_header_style (self ):
124+ style = ""
125+ if "Header Image" in self .headers :
126+ style += "background-image: url('{}'); background-size: cover; " .format (self .headers ['Header Image' ])
127+
128+ if 'Header Color' in self .headers :
129+ color = self .headers ['Header Color' ]
130+ else :
131+ color = '#' + Pckgd .calculate_version_hash (self .filename )[:6 ]
132+ style += "background-color: {}; " .format (color )
133+ return style
134+
135+ def get_update_source (self ):
136+ if not 'Updates From' in self .headers :
137+ return None
138+
139+ if self .headers ['Updates From' ].lower ().startswith ("github" ):
140+ # TODO make this more efficient by caching default branch per repo and make use of other metadata, too.
141+ path = self .headers ['Updates From' ].split ('/' , 3 )
142+ if len (path ) == 4 : # some level of validation...
143+
144+ # query github api to get default branch
145+ url = "https://api.github.com/repos/{}/{}" .format (path [1 ], path [2 ])
146+
147+ response = model .RestGet (url , {"Accept" : "application/vnd.github.v3+json" })
148+ response = json .loads (response )
149+
150+ default_branch = "main"
151+ if 'default_branch' in response :
152+ default_branch = response ['default_branch' ]
153+
154+ return "https://raw.githubusercontent.com/{}/{}/refs/heads/{}/{}" .format (path [1 ], path [2 ], default_branch , path [3 ])
155+
156+ return None
157+
158+ """ Checks if an update is available for this package. Note that this makes at least one, possibly more, HTTP requests. """
159+ def has_update_available (self ):
160+ update_source = self .get_update_source ()
161+ if not update_source :
162+ return False
163+
164+ remote_content = model .RestGet (update_source , {})
165+ if remote_content == "404: Not Found" : # How Github specifically handles these things.
166+ raise Exception ("Update source not found: {}" .format (update_source ))
167+
168+ remote_pkg = Pckgd (self .typeId , self .filename , remote_content )
169+ return remote_pkg .version != self .version
170+
171+
172+ if model .HttpMethod == "get" and Data .v == "" :
173+ model .Header = "Package Manager"
174+ model .Title = "Package Manager"
175+
176+ print ("<h2>Installed Packages</h2>\n " )
177+ print ("<div class=\" section row\" >\n " )
178+
179+ installed = Pckgd .find_installed_packages ()
180+ if (not installed ) or (len (installed ) == 0 ):
181+ print ("<p>No packages are currently installed.<p />\n " )
182+
183+ for p in installed :
184+ print ("<div class=\" package col-12 col-sm-6 col-md-4 col-lg-3\" >\n " )
185+ print ("<div class=\" package-header\" style=\" {}\" ></div>\n " .format (p .get_header_style ()))
186+
187+ print ("<div class=\" package-body\" >\n " )
188+ name = p .headers ['Name' ] if 'Name' in p .headers else p .filename
189+ print ("<h3>{}</h3>\n " .format (name ))
190+ if 'Description' in p .headers :
191+ print ("<p>{}</p>\n " .format (p .headers ['Description' ]))
192+
193+ try :
194+ if p .has_update_available ():
195+ print ("<p><strong>Update available!</strong></p>\n " )
196+ except Exception as e :
197+ print ("<p><strong>Error checking for updates: {}</strong></p>\n " .format (str (e )))
198+
199+ pkg_caps = []
200+ if 'Author' in p .headers :
201+ pkg_caps .append ("by {}" .format (p .headers ['Author' ]))
202+
203+ if 'License' in p .headers :
204+ pkg_caps .append ("<span title=\" License\" >{}</span>" .format (p .headers ['License' ]))
205+
206+ pkg_caps .append ("Version: {}" .format (p .headers ['Version' ]))
207+
208+ print ("<p class=\" package-caption\" >{}</p>\n " .format (" • " .join (pkg_caps )))
209+ print ("</div>\n " )
210+ print ("</div>\n " )
211+
212+ print ("</div>\n " )
213+
214+ # language=html
215+ print ("""
216+ <style>
217+ .package-header {
218+ width: 100%;
219+ padding-top: 25%;
220+ }
221+ .package-body {
222+ padding: 10px;
223+ border-width: 0 1px 1px;
224+ border-style: solid;
225+ border-color: #ccc;
226+ }
227+ div.package-body h3 {
228+ margin-top: 0;
229+ }
230+ p.package-caption {
231+ font-size: 0.75em;
232+ opacity: 0.7;
233+ }
234+ </style>
235+ """ )
0 commit comments