You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Extension.ts 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. import * as Logger from 'log4js'
  2. import * as path from "path";
  3. import { promises as fs } from "fs"
  4. import * as git from "simple-git/promise"
  5. import * as trash from "trash"
  6. Logger.configure({
  7. appenders:
  8. {
  9. "admin/extension": { type: 'stdout' },
  10. //app: { type: 'file', filename: 'application.log' }
  11. },
  12. categories:
  13. {
  14. default: { appenders: [ 'admin/extension' ], level: 'debug' }
  15. }
  16. })
  17. const exec = require('child-process-promise').exec;
  18. const logger = Logger.getLogger("admin/updatemanager")
  19. type SharedStatus = {
  20. name: string
  21. installed?: boolean
  22. outdated?: boolean
  23. }
  24. abstract class Extension<Status extends SharedStatus>{
  25. constructor(
  26. protected name: string
  27. ){}
  28. /**
  29. * the generic Status object
  30. */
  31. protected async status():Promise<Status>{
  32. const installed = await this.isInstalled()
  33. const outdated = await this.isOutdated()
  34. return <Status>{ name: this.name, installed: installed, outdated: outdated }
  35. }
  36. /**
  37. * true => extension was installed
  38. * false => extension was not install
  39. * undefined => signals error
  40. */
  41. abstract async install():Promise<boolean | undefined>
  42. /**
  43. * reverts the install procedure
  44. */
  45. abstract async uninstall():Promise<void>
  46. /**
  47. * true => an update was performed
  48. * false => no update was performed
  49. * undefined => signals error
  50. */
  51. abstract async update(force?: boolean): Promise<boolean | undefined>
  52. /**
  53. * true => update would install a newer version
  54. * false => nothing would be updated if update would be performed
  55. * undefined => signals error
  56. */
  57. abstract async isOutdated():Promise<boolean | undefined>
  58. /**
  59. * promises a boolean
  60. * true => install would do nothing would it be called
  61. * false => install would try to install the newest extension
  62. * undefined => signals error
  63. */
  64. abstract async isInstalled():Promise<boolean | undefined>
  65. }
  66. export type FSStatus = SharedStatus & {
  67. path: string
  68. empty?: boolean
  69. }
  70. export abstract class FSExtension<Status extends FSStatus> extends Extension<Status>{
  71. constructor(
  72. name: string,
  73. protected readonly path: string){
  74. super(name)
  75. }
  76. public async status(): Promise<Status>{
  77. const shdStatus = await super.status()
  78. const empty = await this.isEmpty()
  79. return {...shdStatus, prefix: this.path, empty: empty}
  80. }
  81. public async install():Promise<boolean>{
  82. const installed = await this.isInstalled()
  83. if (!installed) {
  84. logger.info('Installing extension to', path.resolve(this.path, this.name))
  85. await fs.mkdir(path.resolve(this.path, this.name), {recursive: true})
  86. return true
  87. }
  88. logger.error('Extension already installed at', path.resolve(this.path, this.name))
  89. return false
  90. }
  91. public async uninstall(): Promise<void>{
  92. logger.info('Uninstalling extension from', path.resolve(this.path, this.name))
  93. return await trash(path.resolve(this.path, this.name))
  94. }
  95. public async update(force?: boolean): Promise<boolean | undefined>{
  96. if (force) await this.uninstall()
  97. return await this.install()
  98. }
  99. public async isEmpty(): Promise<boolean | undefined>{
  100. if (!this.isInstalled()) return true
  101. try {
  102. const files = await fs.readdir(path.resolve(this.path, this.name))
  103. return files.length === 0
  104. } catch (error) {
  105. logger.error("fs read error", error)
  106. return
  107. }
  108. }
  109. public async isInstalled(): Promise<boolean>{
  110. try {
  111. await fs.access(path.resolve(this.path, this.name))
  112. return true
  113. } catch (error) {
  114. return false
  115. }
  116. }
  117. }
  118. type NPMStatus = FSStatus & {
  119. latest?: string,
  120. current?: string,
  121. author?: string
  122. license?: string,
  123. description?: string
  124. }
  125. export class NPMExtension extends FSExtension<NPMStatus>{
  126. private prefix: string
  127. constructor(
  128. pkgName: string,
  129. prefix: string = './plugins',
  130. private version: string,
  131. ){
  132. super(pkgName, path.resolve(prefix, pkgName, "node_modules"))
  133. this.prefix = (path.resolve(prefix, pkgName))
  134. }
  135. public async status(): Promise<NPMStatus> {
  136. const fsStatus = await super.status()
  137. const latest = await this.getLatestVersion()
  138. const author = await this.getAuthor()
  139. const current = await this.getCurrentVersion()
  140. const license = await this.getLicense()
  141. const description = await this.getDescription()
  142. return {
  143. ...fsStatus,
  144. latest: latest,
  145. author: author,
  146. current: current,
  147. license: license,
  148. description: description
  149. }
  150. }
  151. public async install(): Promise<boolean> {
  152. super.install()
  153. logger.info("npm install", this.name, 'to', path.resolve(this.prefix, this.name))
  154. const { error, stdout, stderr } = await exec('npm install --prefix '+ this.prefix + ' ' + this.name+"@"+this.version)
  155. if (error) {
  156. logger.error('npm install', this.name, "@", this.version, ' from prefix', this.prefix, 'stderr:', stderr)
  157. return false
  158. } else {
  159. logger.info('npm install', this.name, "@", this.version, 'from prefix', this.prefix, 'stdout:', stdout)
  160. return true
  161. }
  162. }
  163. public async uninstall(force?: boolean): Promise<void> {
  164. super.uninstall()
  165. logger.info("npm uninstall", this.name, 'from', path.resolve(this.prefix, this.name))
  166. const { error, stdout, stderr } = await exec('npm uninstall --prefix '+ this.prefix + ' ' + this.name)
  167. if (error) {
  168. logger.error('npm uninstall', this.name, 'from prefix', this.prefix, 'stderr:', stderr)
  169. } else {
  170. logger.info('npm uninstall', this.name, 'from prefix', this.prefix, 'stdout:', stdout)
  171. }
  172. }
  173. public async update(force?: boolean): Promise<boolean> {
  174. super.update(force)
  175. logger.info("npm update", path.resolve(this.prefix, this.name))
  176. const { error, stdout, stderr } = await exec('npm update --prefix '+ this.prefix + ' ' + this.name)
  177. if (error) {
  178. logger.error('npm update', this.name, 'from prefix', this.prefix, 'stderr:', stderr)
  179. return false
  180. } else {
  181. logger.info('npm update', this.name, 'from prefix', this.prefix, 'stdout:', stdout)
  182. return true
  183. }
  184. }
  185. public async isOutdated(): Promise<boolean | undefined> {
  186. try {
  187. const {
  188. error,
  189. stdout,
  190. stderr } = await exec('npm outdated --prefix ' + this.prefix + " --json" + ' ' + this.name)
  191. if (error) throw new Error(stderr)
  192. if (Object.keys(JSON.parse(stdout)).length > 0) return true
  193. else return false
  194. } catch (error) {
  195. logger.error('npm outdated', this.name, 'from prefix', this.prefix, 'stderr:', error)
  196. return
  197. }
  198. }
  199. private async getAuthor(): Promise<string | undefined>{
  200. const {
  201. error,
  202. stdout,
  203. stderr } = await exec('npm author ls --prefix ' + this.prefix + ' ' + this.name)
  204. if(error){
  205. logger.error(stderr)
  206. return
  207. } else {
  208. logger.info(stdout)
  209. return stdout
  210. }
  211. }
  212. private async getLatestVersion(): Promise<string | undefined>{
  213. try{
  214. const {
  215. error,
  216. stdout,
  217. stderr } = await exec('npm view --prefix ' + this.prefix + ' ' + this.name + " version")
  218. if(error){
  219. logger.error(stderr)
  220. return
  221. } else {
  222. logger.info(stdout)
  223. return stdout
  224. }
  225. }catch(error){
  226. logger.error(error)
  227. return
  228. }
  229. }
  230. private async getCurrentVersion(): Promise<string | undefined>{
  231. try{
  232. const {
  233. error,
  234. stdout,
  235. stderr } = await exec('npm list --json --prefix ' + this.prefix + ' ' + this.name)
  236. if(error){
  237. logger.error(stderr)
  238. return
  239. } else {
  240. logger.info(stdout)
  241. return JSON.parse(stdout)['dependencies'][this.name]['version']
  242. }
  243. }catch(error){
  244. logger.error(error)
  245. return
  246. }
  247. }
  248. private async getDescription(): Promise<string | undefined>{
  249. try{
  250. const {
  251. error,
  252. stdout,
  253. stderr } = await exec('npm show --prefix ' + this.prefix + ' ' + this.name+" description")
  254. if(error){
  255. logger.error(stderr)
  256. return
  257. } else {
  258. logger.info(stdout)
  259. return stdout
  260. }
  261. }catch(error){
  262. logger.error(error)
  263. return
  264. }
  265. }
  266. private async getLicense(): Promise<string | undefined>{
  267. try{
  268. const {
  269. error,
  270. stdout,
  271. stderr } = await exec('npm show --prefix ' + this.prefix + ' ' + this.name+" license")
  272. if(error){
  273. logger.error(stderr)
  274. return
  275. } else {
  276. logger.info(stdout)
  277. return stdout
  278. }
  279. }catch(error){
  280. logger.error(error)
  281. return
  282. }
  283. }
  284. }
  285. export type GitStatus = FSStatus & {
  286. branch?: string
  287. remote?: string
  288. latest?: string
  289. }
  290. export class GitExtension<Status extends GitStatus> extends FSExtension<Status>{
  291. protected repo: git.SimpleGit
  292. constructor(
  293. repoName:string,
  294. localPrefix:string,
  295. protected gitCloneURL: string,
  296. protected credentials?: [string, string]
  297. ){
  298. super(repoName, localPrefix)
  299. }
  300. public async status(): Promise<Status>{
  301. const fsStatus = await super.status()
  302. return {
  303. ...fsStatus,
  304. branch: 'wat',
  305. remote: this.gitCloneURL,
  306. latest: 'kek'
  307. }
  308. }
  309. public async install(): Promise<boolean> {
  310. if (super.install()){
  311. if (this.credentials){
  312. } else {
  313. }
  314. } else {
  315. }
  316. return false;
  317. }
  318. public async update(): Promise<boolean> {
  319. return true // do git pull remote origin && git checkout
  320. }
  321. public async isOutdated(): Promise<boolean> {
  322. return true
  323. }
  324. }
  325. export type PluginStatus = GitStatus & {
  326. hasFrontend: boolean,
  327. hasBackend: boolean,
  328. isFrontendLoaded: boolean,
  329. isBackendLoaded: boolean
  330. }
  331. export class PluginExtension<Status extends GitStatus> extends GitExtension<Status> {
  332. protected frontendLoaded: boolean
  333. protected backendLoaded: boolean
  334. constructor(
  335. pluginName: string,
  336. gitCloneURL: string,
  337. localPrefix: string = './plugins',
  338. credentials?: [string, string],
  339. protected hasBackend: boolean = true,
  340. protected hasFrontend: boolean = true){
  341. super(pluginName,localPrefix,gitCloneURL,credentials)
  342. }
  343. public async load(): Promise<void>{
  344. if (this.hasFrontend) await this.loadFrontend()
  345. if (this.hasBackend) await this.loadBackend()
  346. }
  347. public async loadFrontend(): Promise<void>{
  348. return //TODO
  349. }
  350. public async loadBackend(): Promise<void>{
  351. return //TODO
  352. }
  353. public async unload(): Promise<void>{
  354. if (this.hasFrontend) await this.unloadFrontend()
  355. if (this.hasBackend) await this.unloadBackend()
  356. }
  357. public async unloadFrontend(): Promise<void>{
  358. return //TODO
  359. }
  360. public async unloadBackend(): Promise<void>{
  361. return //TODO
  362. }
  363. }