diff options
author | Simon Rettberg | 2019-10-09 17:31:19 +0200 |
---|---|---|
committer | Simon Rettberg | 2019-10-09 17:31:19 +0200 |
commit | f800abeea4f6c68182c51cd4aaea19d7636431c8 (patch) | |
tree | 802f9bcee14210b355cac211751449e31c71fbc0 /modules-available/minilinux | |
parent | slx-fixes: Expose function for adding additional automatic confirm handlers (diff) | |
download | slx-admin-f800abeea4f6c68182c51cd4aaea19d7636431c8.tar.gz slx-admin-f800abeea4f6c68182c51cd4aaea19d7636431c8.tar.xz slx-admin-f800abeea4f6c68182c51cd4aaea19d7636431c8.zip |
[minilinux] Rewrite for multiple version/sources handling
* You can supply multiple sources for updates (URLs)
* Sources can provide multiple branches
* Each branch can supply multiple versions (eg. updates)
TODO: Set global default version
TODO: Supply hook to serversetup-ipxe to add specific boot entries
TODO: UX polish
TODO: phpdoc/polish
Diffstat (limited to 'modules-available/minilinux')
17 files changed, 764 insertions, 212 deletions
diff --git a/modules-available/minilinux/hooks/cron.inc.php b/modules-available/minilinux/hooks/cron.inc.php new file mode 100644 index 00000000..fa6e4c70 --- /dev/null +++ b/modules-available/minilinux/hooks/cron.inc.php @@ -0,0 +1,6 @@ +<?php + +$ret = explode(':', date('G:i')); +if ((int)$ret[0] === 6 && (int)$ret[1] < 5) { + MiniLinux::updateList(); +}
\ No newline at end of file diff --git a/modules-available/minilinux/hooks/ipxe-bootentry.inc.php b/modules-available/minilinux/hooks/ipxe-bootentry.inc.php new file mode 100644 index 00000000..4e2cbb5e --- /dev/null +++ b/modules-available/minilinux/hooks/ipxe-bootentry.inc.php @@ -0,0 +1,42 @@ +<?php + +class Wurst extends BootEntryHook +{ + + public function name() + { + return 'Wurst'; + } + + /** + * @return HookEntryGroup[] + */ + protected function groupsInternal() + { + return [ + new HookEntryGroup('Senf Gruppe', [ + new HookEntry('senf-1', 'Senf v1'), + new HookEntry('senf-2', 'Senf v2'), + ]), + new HookEntryGroup('Schnecke Gruppe', [ + new HookEntry('s-1', 'Trulla'), + new HookEntry('s-2', 'Herbert'), + ]), + ]; + } + + /** + * @param $id + * @return BootEntry the actual boot entry instance for given entry, false if invalid id + */ + public function getBootEntry($id) + { + $bios = new ExecData(); + $bios->executable = 'mspaint.exe'; + $bios->initRd = 'www.google.de'; + $bios->commandLine = '-q'; + return BootEntry::newStandardBootEntry($bios, false, 'agnostic'); + } +} + +return new Wurst();
\ No newline at end of file diff --git a/modules-available/minilinux/inc/minilinux.inc.php b/modules-available/minilinux/inc/minilinux.inc.php new file mode 100644 index 00000000..46c63c94 --- /dev/null +++ b/modules-available/minilinux/inc/minilinux.inc.php @@ -0,0 +1,228 @@ +<?php + +class MiniLinux +{ + + const PROPERTY_KEY_FETCHTIME = 'ml-list-fetch'; + + public static function updateList() + { + $stamp = time(); + $last = Property::get(self::PROPERTY_KEY_FETCHTIME); + error_log('Last: ' . $last); + if ($last !== false && $last + 10 > $stamp) + return 0; // In progress... + Property::set(self::PROPERTY_KEY_FETCHTIME, $stamp, 1); + Database::exec('LOCK TABLES callback WRITE, + minilinux_source WRITE, minilinux_branch WRITE, minilinux_version WRITE'); + Database::exec('UPDATE minilinux_source SET taskid = UUID()'); + $cutoff = time() - 3600; + Database::exec("UPDATE minilinux_version + INNER JOIN minilinux_branch USING (branchid) + INNER JOIN minilinux_source USING (sourceid) + SET orphan = orphan + 1 WHERE minilinux_source.lastupdate < $cutoff"); + $list = Database::queryAll('SELECT sourceid, url, taskid FROM minilinux_source'); + foreach ($list as $source) { + Taskmanager::submit('DownloadText', array( + 'id' => $source['taskid'], + 'url' => $source['url'], + ), true); + TaskmanagerCallback::addCallback($source['taskid'], 'mlDownload', $source['sourceid']); + } + Database::exec('UNLOCK TABLES'); + return count($list); + } + + public static function listDownloadCallback($task, $sourceid) + { + $taskId = $task['id']; + $data = json_decode($task['data']['content'], true); + if (!is_array($data)) { + EventLog::warning('Cannot download Linux version meta data for ' . $sourceid); + error_log(print_r($task, true)); + $lastupdate = 'lastupdate'; + } else { + if (isset($data['systems']) && is_array($data['systems'])) { + self::addBranches($sourceid, $data['systems']); + } + $lastupdate = 'UNIX_TIMESTAMP()'; + } + Database::exec("UPDATE minilinux_source SET lastupdate = $lastupdate, taskid = NULL + WHERE sourceid = :sourceid AND taskid = :taskid", + ['sourceid' => $sourceid, 'taskid' => $taskId]); + } + + private static function addBranches($sourceid, $systems) + { + foreach ($systems as $system) { + if (!self::isValidIdPart($system['id'])) + continue; + $branchid = $sourceid . '/' . $system['id']; + $title = empty($system['title']) ? $branchid : $system['title']; + $description = empty($system['description']) ? '' : $system['description']; + Database::exec('INSERT INTO minilinux_branch (branchid, sourceid, title, description) + VALUES (:branchid, :sourceid, :title, :description) + ON DUPLICATE KEY UPDATE title = VALUES(title), description = VALUES(description)', [ + 'branchid' => $branchid, + 'sourceid' => $sourceid, + 'title' => $title, + 'description' => $description, + ]); + if (isset($system['versions']) && is_array($system['versions'])) { + self::addVersions($branchid, $system['versions']); + } + } + } + + private static function addVersions($branchid, $versions) + { + foreach ($versions as $version) { + self::addVersion($branchid, $version); + } + } + + private static function addVersion($branchid, $version) + { + if (!self::isValidIdPart($version['version'])) { + error_log("Ignoring version {$version['version']} from $branchid: Invalid characters in ID"); + return; + } + if (empty($version['files']) && empty($version['cmdline'])) { + error_log("Ignoring version {$version['version']} from $branchid: Neither file list nor command line"); + return; + } + $versionid = $branchid . '/' . $version['version']; + $title = empty($version['title']) ? '' : $version['title']; + $dateline = empty($version['releasedate']) ? time() : (int)$version['releasedate']; + unset($version['version'], $version['title'], $version['releasedate']); + // Sanitize files array + if (!isset($version['files']) || !is_array($version['files'])) { + unset($version['files']); + } else { + foreach (array_keys($version['files']) as $key) { + $file =& $version['files'][$key]; + if (empty($file['name'])) { + error_log("Ignoring version {$version['version']} from $branchid: Entry in file list has missing file name"); + return; + } + if ($file['name'] === 'menu.txt' || $file['name'] === 'menu-debug.txt') { + unset($version['files'][$key]); + continue; + } + if (empty($file['gpg'])) { + error_log("Ignoring version {$version['version']} from $branchid: {$file['name']} has no GPG signature"); + return; + } + if (preg_match(',/\.\.|\.\./|/|\x00,', $file['name']) > 0) { // Invalid chars + error_log("Ignoring version {$version['version']} from $branchid: {$file['name']} contains invalid characters"); + return; + } + if (isset($file['md5'])) { + $file['md5'] = strtolower($file['md5']); + } + } + unset($file); + $version['files'] = array_values($version['files']); + } + $data = json_encode($version); + Database::exec('INSERT INTO minilinux_version (versionid, branchid, title, dateline, data, orphan) + VALUES (:versionid, :branchid, :title, :dateline, :data, 0) + ON DUPLICATE KEY UPDATE title = VALUES(title), data = VALUES(data), orphan = 0', [ + 'versionid' => $versionid, + 'branchid' => $branchid, + 'title' => $title, + 'dateline' => $dateline, + 'data' => $data, + ]); + } + + private static function isValidIdPart($str) + { + return preg_match('/^[a-z0-9_\-]+$/', $str) > 0; + } + + public static function validateDownloadTask($versionid, $taskid) + { + if ($taskid === null) + return false; + $task = Taskmanager::status($taskid); + if (Taskmanager::isTask($task) && !Taskmanager::isFailed($task) + && (is_dir(CONFIG_HTTP_DIR . '/' . $versionid) || !Taskmanager::isFinished($task))) + return $task['id']; + Database::exec('UPDATE minilinux_version SET taskid = NULL + WHERE versionid = :versionid AND taskid = :taskid', + ['versionid' => $versionid, 'taskid' => $taskid]); + return false; + } + + /** + * Download the files for the given version id + * @param $versionid + * @return bool + */ + public static function downloadVersion($versionid) + { + $ver = Database::queryFirst('SELECT s.url, s.pubkey, v.versionid, v.taskid, v.data FROM minilinux_version v + INNER JOIN minilinux_branch b USING (branchid) + INNER JOIN minilinux_source s USING (sourceid) + WHERE versionid = :versionid', + ['versionid' => $versionid]); + if ($ver === false) + return false; + $taskid = self::validateDownloadTask($versionid, $ver['taskid']); + if ($taskid !== false) + return $taskid; + $data = json_decode($ver['data'], true); + if (!is_array($data)) { + EventLog::warning("Cannot download Linux '$versionid': Corrupted meta data.", $ver['data']); + return false; + } + if (empty($data['files'])) + return false; + $list = []; + $legacyDir = preg_replace(',^[^/]*/,', '', $versionid); + foreach ($data['files'] as $file) { + if (empty($file['name'])) + continue; + $list[] = [ + 'id' => self::fileToId($versionid, $file['name']), + 'url' => empty($file['url']) + ? ($ver['url'] . '/' . $legacyDir . '/' . $file['name']) + : ($ver['url'] . '/' . $file['url']), + 'fileName' => $file['name'], + 'gpg' => $file['gpg'], + ]; + } + error_log(print_r($list, true)); + $uuid = Util::randomUuid(); + Database::exec('LOCK TABLES minilinux_version WRITE'); + $aff = Database::exec('UPDATE minilinux_version SET taskid = :taskid WHERE versionid = :versionid AND taskid IS NULL', + ['taskid' => $uuid, 'versionid' => $versionid]); + if ($aff > 0) { + $task = Taskmanager::submit('DownloadFiles', [ + 'id' => $uuid, + 'baseDir' => CONFIG_HTTP_DIR . '/' . $versionid, + 'gpgPubKey' => $ver['pubkey'], + 'files' => $list, + ]); + if (Taskmanager::isFailed($task)) { + error_log(print_r($task, true)); + $task = false; + } else { + $task = $task['id']; + } + } else { + $task = false; + } + Database::exec('UNLOCK TABLES'); + if ($aff === 0) + return self::downloadVersion($versionid); + return $task; + } + + public static function fileToId($versionid, $fileName) + { + return 'x' . substr(md5($fileName . $versionid), 0, 8); + } + +}
\ No newline at end of file diff --git a/modules-available/minilinux/install.inc.php b/modules-available/minilinux/install.inc.php new file mode 100644 index 00000000..50be13a5 --- /dev/null +++ b/modules-available/minilinux/install.inc.php @@ -0,0 +1,40 @@ +<?php + +$result[] = tableCreate('minilinux_source', " + `sourceid` varchar(8) CHARACTER SET ascii NOT NULL, + `title` varchar(100) NOT NULL, + `url` varchar(200) NOT NULL, + `lastupdate` int(10) UNSIGNED NOT NULL, + `taskid` char(36) CHARACTER SET ascii DEFAULT NULL, + `pubkey` blob NOT NULL, + PRIMARY KEY (`sourceid`), + KEY (`title`, `sourceid`) +"); +$result[] = tableCreate('minilinux_branch', " + `sourceid` varchar(8) CHARACTER SET ascii DEFAULT NULL, + `branchid` varchar(40) CHARACTER SET ascii NOT NULL, + `title` varchar(100) NOT NULL, + `description` blob NOT NULL, + PRIMARY KEY (`branchid`), + KEY (`title`) +"); +$result[] = tableCreate('minilinux_version', " + `branchid` varchar(40) CHARACTER SET ascii NOT NULL, + `versionid` varchar(72) CHARACTER SET ascii NOT NULL, + `title` varchar(100) NOT NULL, + `dateline` int(10) UNSIGNED NOT NULL, + `data` blob NOT NULL, + `orphan` tinyint(3) UNSIGNED NOT NULL, + `taskid` char(36) CHARACTER SET ascii DEFAULT NULL, + PRIMARY KEY (`versionid`), + KEY (`title`), + KEY (`branchid`, `dateline`, `versionid`) +"); + +$result[] = tableAddConstraint('minilinux_version', 'branchid', 'minilinux_branch', 'branchid', + 'ON UPDATE CASCADE ON DELETE CASCADE'); + +$result[] = tableAddConstraint('minilinux_branch', 'sourceid', 'minilinux_source', 'sourceid', + 'ON UPDATE CASCADE ON DELETE SET NULL'); + +responseFromArray($result); diff --git a/modules-available/minilinux/lang/de/messages.json b/modules-available/minilinux/lang/de/messages.json index c91772c7..1c38a8c1 100644 --- a/modules-available/minilinux/lang/de/messages.json +++ b/modules-available/minilinux/lang/de/messages.json @@ -1,3 +1,6 @@ { - "please-download-minilinux": "Wichtige Dateien der MiniLinux-Installation fehlen." + "delete-error": "Fehler beim L\u00f6schen der Version {{0}}: {{1}}", + "no-such-version": "Ung\u00fcltige\/Unbekannte Version: {{0}}", + "please-download-minilinux": "Wichtige Dateien der MiniLinux-Installation fehlen", + "version-deleted": "Version {{0}} wurde gel\u00f6scht" }
\ No newline at end of file diff --git a/modules-available/minilinux/lang/de/module.json b/modules-available/minilinux/lang/de/module.json index e5284ac0..3ad539ca 100644 --- a/modules-available/minilinux/lang/de/module.json +++ b/modules-available/minilinux/lang/de/module.json @@ -1,4 +1,8 @@ { + "file-checksum-bad": "Pr\u00fcfsummenfehler", + "file-missing": "Datei fehlt", + "file-ok": "OK", + "file-size-mismatch": "Dateigr\u00f6\u00dfe stimmt nicht", "module_name": "bwLehrpool MiniLinux", - "page_title": "MiniLinux verwalten und aktualisieren" + "page_title": "Linuxvarianten f\u00fcr Netboot verwalten" }
\ No newline at end of file diff --git a/modules-available/minilinux/lang/de/template-tags.json b/modules-available/minilinux/lang/de/template-tags.json index 60a11db9..144887c8 100644 --- a/modules-available/minilinux/lang/de/template-tags.json +++ b/modules-available/minilinux/lang/de/template-tags.json @@ -1,15 +1,18 @@ { - "lang_canUpdate1": "Mindestens eine Komponente von", - "lang_canUpdate2": "kann aktualisiert werden. F\u00fcr einen reibungslosen Betrieb wird empfohlen, alle Komponenten auf dem aktuellen Stand zu halten.", - "lang_configurationPackageNotFound": "Keine Konfigurationspakete gefunden!", - "lang_desiredVersion": "Gew\u00fcnschte Version", - "lang_errorGetting": "Fehler beim Herunterladen der Liste!", - "lang_filesInVersion": "Dateien zu Version", - "lang_listObtained": "Liste wird heruntergeladen...", - "lang_outdated": "Veraltet", - "lang_redownload": "Erneut herunterladen", - "lang_systemUpdated": "Das System ist auf dem aktuellen Stand.", - "lang_update": "Aktualisieren", - "lang_updateAll": "Alle Module aktualisieren", - "lang_uptodate": "Aktuell" + "lang_confirmDeleteVersion": "Diese Version wirklich l\u00f6schen?", + "lang_download": "Herunterladen", + "lang_id": "ID", + "lang_introText": "Hier gibts MiniLinux.", + "lang_key": "GPG-Key", + "lang_lastUpdate": "Zuletzt \u00fcberpr\u00fcft", + "lang_minilinuxHeading": "Netboot Linux verwalten", + "lang_orphanedVersion": "Verwaiste Version", + "lang_releaseDate": "Ver\u00f6ffentlichungsdatum", + "lang_sources": "Quellen", + "lang_title": "Titel", + "lang_updateSourcesButton": "Nach neuen Updates suchen", + "lang_url": "URL", + "lang_verify": "Integrit\u00e4t \u00fcberpr\u00fcfen", + "lang_verifyToolTip": "Dateiintegrit\u00e4t anhand von Pr\u00fcfsummen verifizieren", + "lang_version": "Version" }
\ No newline at end of file diff --git a/modules-available/minilinux/lang/en/messages.json b/modules-available/minilinux/lang/en/messages.json index 8d7fa76d..6dc736a4 100644 --- a/modules-available/minilinux/lang/en/messages.json +++ b/modules-available/minilinux/lang/en/messages.json @@ -1,3 +1,6 @@ { - "please-download-minilinux": "Important files from the mini Linux installation are missing." + "delete-error": "Error deleting version {{0}}: {{1}}", + "no-such-version": "No such version: {{0}}", + "please-download-minilinux": "Important files from the mini Linux installation are missing.", + "version-deleted": "Deleted version {{0}}" }
\ No newline at end of file diff --git a/modules-available/minilinux/lang/en/module.json b/modules-available/minilinux/lang/en/module.json index 01cb3d00..899435e7 100644 --- a/modules-available/minilinux/lang/en/module.json +++ b/modules-available/minilinux/lang/en/module.json @@ -1,3 +1,8 @@ { - "module_name": "Minilinux" + "file-checksum-bad": "Bad checksum", + "file-missing": "File missing", + "file-ok": "OK", + "file-size-mismatch": "File size mismatch", + "module_name": "Minilinux", + "page_title": "Manage Netboot Linux flavors" }
\ No newline at end of file diff --git a/modules-available/minilinux/page.inc.php b/modules-available/minilinux/page.inc.php index e973ee6e..b54050bf 100644 --- a/modules-available/minilinux/page.inc.php +++ b/modules-available/minilinux/page.inc.php @@ -12,120 +12,225 @@ class Page_MiniLinux extends Page Util::redirect('?do=Main'); } + if (Request::isPost()) { + $show = Request::post('show', false, 'string'); + if ($show === 'delete') { + $this->deleteVersion(); + } elseif ($show === 'updatesources') { + $this->updateSources(); + } + Util::redirect('?do=minilinux'); + } + User::assertPermission('view'); } protected function doRender() { - Render::addTemplate('page-minilinux', array( - 'listurl' => '?do=MiniLinux&async=true&action=list' - )); + Render::addTemplate('page-minilinux'); + // List branches and versions + $branches = Database::queryAll('SELECT sourceid, branchid, title FROM minilinux_branch ORDER BY title ASC'); + $versions = $this->queryAllVersionsByBranch(); + foreach ($branches as &$branch) { + if (isset($versions[$branch['branchid']])) { + $branch['versionlist'] = $this->renderVersionList($versions[$branch['branchid']]); + } + } + Render::addTemplate('branches', ['branches' => $branches]); + // List sources + $res = Database::simpleQuery('SELECT sourceid, title, url, lastupdate, pubkey FROM minilinux_source ORDER BY title, sourceid'); + $data = ['list' => [], 'show_refresh' => true]; + $tooOld = strtotime('-7 days'); + $showRefresh = strtotime('-10 minutes'); + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + $row['lastupdate_s'] = Util::prettyTime($row['lastupdate']); + if ($row['lastupdate'] != 0 && $row['lastupdate'] < $tooOld) { + $row['update_class'] = 'text-danger'; + } + if ($row['lastupdate'] > $showRefresh) { + $data['show_refresh'] = false; + } + $data['list'][] = $row; + } + Render::addTemplate('sources', $data); } protected function doAjax() { - $data = Property::getVersionCheckInformation(); - if (!is_array($data) || !isset($data['systems'])) { - echo Render::parse('messagebox', array( - 'type' => 'danger', - 'message' => 'Failed to retrieve the list: ' . print_r($data, true) - ), 'main'); - return; + User::load(); + $show = Request::post('show', false, 'string'); + if ($show === 'version') { + $this->ajaxVersion(); + } elseif ($show === 'download') { + $this->ajaxDownload(); } - $action = Request::any('action'); - $selectedVersion = (int)Request::any('version', 0); - switch ($action) { - case 'list': - $count = 0; - foreach ($data['systems'] as &$system) { - // Get latest version, build simple array of all version numbers - $versionNumbers = array(); - $selected = false; - foreach ($system['versions'] as $version) { - if (!is_numeric($version['version']) || $version['version'] < 1) - continue; - if ($selectedVersion === 0 && ($selected === false || $selected['version'] < $version['version'])) - $selected = $version; - elseif ($version['version'] == $selectedVersion) - $selected = $version; - $versionNumbers[(int)$version['version']] = array( - 'version' => $version['version'] - ); + } + + private function queryAllVersionsByBranch() + { + $list = []; + $res = Database::simpleQuery('SELECT branchid, versionid, title, dateline, orphan, taskid + FROM minilinux_version ORDER BY branchid, dateline, versionid'); + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + $list[$row['branchid']][] = $row; + } + return $list; + } + + private function renderVersionList($versions) + { + foreach ($versions as &$version) { + $version['dateline_s'] = Util::prettyTime($version['dateline']); + $version['orphan'] = ($version['orphan'] > 5); + $version['installed'] = is_dir(CONFIG_HTTP_DIR . '/' . $version['versionid']); + $version['downloading'] = $version['taskid'] && Taskmanager::isRunning(Taskmanager::status($version['taskid'])); + } + return Render::parse('versionlist', ['versions' => $versions]); + } + + private function ajaxVersion() + { + User::assertPermission('view'); + $verify = Request::post('verify', false, 'bool'); + $versionid = Request::post('version', false, 'string'); + if ($versionid === false) { + die('What!'); + } + $ver = Database::queryFirst('SELECT versionid, taskid, data FROM minilinux_version WHERE versionid = :versionid', + ['versionid' => $versionid]); + if ($ver === false) { + die('No such version'); + } + $versionid = $ver['versionid']; // Just to be sure -- should be safe for building a path either way + $data = json_decode($ver['data'], true); + if (!is_array($data)) { + die('Corrupted data'); + } + $data['versionid'] = $versionid; + $data['dltask'] = MiniLinux::validateDownloadTask($versionid, $ver['taskid']); + $data['verify_button'] = !$verify && $data['dltask'] === false; + if (is_array($data['files'])) { + $sort = []; + foreach ($data['files'] as &$file) { + if (empty($file['name'])) { + $sort[] = 'zzz' . implode(',', $file); + continue; } - if ($selected === false) continue; // No versions for this system!? - ksort($versionNumbers); - // Mark latest version as selected - $versionNumbers[(int)$selected['version']]['selected'] = true; - // Add status information to system and its files - foreach ($selected['files'] as &$file) { - $file['uid'] = 'dlid' . $count++; - $local = CONFIG_HTTP_DIR . '/' . $system['id'] . '/' . $file['name']; - if (!file_exists($local) || filesize($local) !== $file['size'] || filemtime($local) < $file['mtime'] - || md5_file($local) !== $file['md5']) { - $file['fileChanged'] = true; - $system['systemChanged'] = true; - } - $taskId = Property::getDownloadTask($file['md5']); - if ($taskId !== false) { - $task = Taskmanager::status($taskId); - if (isset($task['data']['progress'])) { - $file['download'] = Render::parse('download', array( - 'task' => $taskId, - 'name' => $file['name'] - )); - } - } + $sort[] = $file['name']; + $s = $this->getFileState($versionid, $file, $verify); + if ($s !== self::FILE_OK) { + $data['verify_button'] = false; + $data['download_button'] = !$data['dltask']; } - unset($system['versions']); - $system['files'] = $selected['files']; - $system['version'] = $selected['version']; - } - $data['versions'] = array_values($versionNumbers); - Permission::addGlobalTags($data['perms'], null, ['update']); - echo Render::parse('filelist', $data); - return; - case 'download': - User::assertPermission('update'); - $id = Request::post('id'); - $name = Request::post('name'); - if (!$id || !$name || strpos("$id$name", '/') !== false) { - echo "Invalid download request"; - return; - } - $file = false; - $gpg = 'missing'; - foreach ($data['systems'] as &$system) { - if ($system['id'] !== $id) continue; - foreach ($system['versions'] as &$version) { - if ($version['version'] != $selectedVersion) continue; - foreach ($version['files'] as &$f) { - if ($f['name'] !== $name) continue; - $file = $f; - if (!empty($f['gpg'])) $gpg = $f['gpg']; - break; - } + if ($s !== self::FILE_MISSING) { + $data['delete_button'] = true; + } + $file['state'] = $this->fileStateToString($s); + if (isset($file['size'])) { + $file['size_s'] = Util::readableFileSize($file['size']); + } + if (isset($file['mtime'])) { + $file['mtime_s'] = Util::prettyTime($file['mtime']); + } + if ($data['dltask']) { + $file['fileid'] = MiniLinux::fileToId($versionid, $file['name']); } } - if ($file === false) { - echo "Nonexistent system/file: $id / $name"; - return; - } - $task = Taskmanager::submit('DownloadFile', array( - 'url' => CONFIG_REMOTE_ML . '/' . $id . '/' . $selectedVersion . '/' . $name, - 'destination' => CONFIG_HTTP_DIR . '/' . $id . '/' . $name, - 'gpg' => $gpg - )); - if (!isset($task['id'])) { - echo 'Error launching download task: ' . $task['statusCode']; - return; + unset($file); + array_multisort($sort, SORT_ASC, $data['files']); + } + echo Render::parse('filelist', $data); + } + + const FILE_OK = 0; + const FILE_MISSING = 1; + const FILE_SIZE_MISMATCH = 2; + const FILE_CHECKSUM_BAD = 3; + + private function getFileState($versionid, $file, $verify) + { + $path = CONFIG_HTTP_DIR . '/' . $versionid . '/' . $file['name']; + if (!is_file($path)) + return self::FILE_MISSING; + if (isset($file['size']) && filesize($path) != $file['size']) + return self::FILE_SIZE_MISMATCH; + if ($verify) { + // TODO: Others + if (isset($file['md5'])) { + if (md5_file($path) !== $file['md5']) + return self::FILE_CHECKSUM_BAD; } - Property::setDownloadTask($file['md5'], $task['id']); - echo Render::parse('download', array( - 'name' => $name, - 'task' => $task['id'] - )); + } + return self::FILE_OK; + } + + private function fileStateToString($state) + { + switch ($state) { + case self::FILE_CHECKSUM_BAD: + return Dictionary::translate('file-checksum-bad', true); + case self::FILE_SIZE_MISMATCH: + return Dictionary::translate('file-size-mismatch', true); + case self::FILE_MISSING: + return Dictionary::translate('file-missing', true); + case self::FILE_OK: + return Dictionary::translate('file-ok', true); + } + return '???'; + } + + private function ajaxDownload() + { + User::assertPermission('update'); + $version = Request::post('version', false, 'string'); + if ($version === false) { + die('No version'); + } + $task = MiniLinux::downloadVersion($version); + if ($task === false) { + Message::addError('no-such-version', $version); + Message::renderList(); + } else { + $this->ajaxVersion(); + } + } + + private function deleteVersion() + { + User::assertPermission('delete'); + $versionid = Request::post('version', false, 'string'); + if ($versionid === false) { + Message::addError('main.parameter-missing', 'versionid'); + return; + } + $version = Database::queryFirst('SELECT versionid FROM minilinux_version WHERE versionid = :versionid', + ['versionid' => $versionid]); + if ($version === false) { + Message::addError('no-such-version'); return; } + $path = CONFIG_HTTP_DIR . '/' . $version['versionid']; + $task = Taskmanager::submit('DeleteDirectory', [ + 'path' => $path, + 'recursive' => true, + ]); + if ($task !== false) { + $task = Taskmanager::waitComplete($task, 2500); + if (Taskmanager::isFailed($task)) { + Message::addError('delete-error', $versionid, $task['data']['error']); + } else { + Message::addSuccess('version-deleted', $versionid); + } + } + } + + private function updateSources() + { + $ret = MiniLinux::updateList(); + if ($ret > 0) { + sleep(2); + Trigger::checkCallbacks(); + } } } diff --git a/modules-available/minilinux/permissions/permissions.json b/modules-available/minilinux/permissions/permissions.json index b018ee72..ec311047 100644 --- a/modules-available/minilinux/permissions/permissions.json +++ b/modules-available/minilinux/permissions/permissions.json @@ -4,5 +4,8 @@ }, "update": { "location-aware": false + }, + "delete": { + "location-aware": false } }
\ No newline at end of file diff --git a/modules-available/minilinux/templates/branches.html b/modules-available/minilinux/templates/branches.html new file mode 100644 index 00000000..1ba9497c --- /dev/null +++ b/modules-available/minilinux/templates/branches.html @@ -0,0 +1,73 @@ +{{#branches}} +<div id="ibm-mainframe" class="panel panel-default"> + <div class="panel-heading"> + <div class="pull-right"> + {{sourceid}} {{branchid}} + </div> + {{title}} + </div> + <div class="panel-body"> + + </div> + {{{versionlist}}} +</div> +{{/branches}} +<script> + document.addEventListener('DOMContentLoaded', function () { + var addHandlers = function(parent) { + parent.find('.btn-verify').click(function() { + loadDetails($(this).data('version'), { show: "version", verify: 1 }); + }); + parent.find('.btn-download').click(function() { + loadDetails($(this).data('version'), { show: "download" }); + $(this).remove(); + }); + }; + var loadDetails = function(version, params) { + var c = $('.version-container[data-version="' + version + '"]'); + c.show(); + if (c.is(':empty')) { + c.html('<span class="glyphicon glyphicon-refresh slx-rotation"></span>'); + } else { + c.addClass('slx-fade'); + c.find('button, a').addClass('disabled').prop('disabled', true); + } + var data = { version: version, token: TOKEN }; + $.extend(data, params); + c.load('?do=minilinux', data, + function () { + c.removeClass('slx-fade'); + addHandlers(c); + c.find('button[data-confirm]').click(slxModalConfirmHandler); + tmInit(); + }); + }; + $('.version-link').click(function(e) { + e.preventDefault(); + $(this).removeClass('version-link').off().removeAttr('href'); + loadDetails($(this).data('version'), { show: "version" }); + }); + addHandlers($('#ibm-mainframe')); + $('[data-autoclick="true"]').click(); + }); + var taskDone = {}; + function dlTmCb(task) { + if (!task.data || !task.data.files) + return; + for (var i = 0; i < task.data.files.length; ++i) { + var f = task.data.files[i]; + var id = task.id + f.id; + if (taskDone[id] === true) + continue; + var $div = $('#' + f.id); + if (f.error) { + $div.text(f.error).addClass('text-danger'); + taskDone[id] = true; + } else { + var wasDone = (taskDone[id] === 100); + tmSetProgress($div, f.progress, wasDone ? 'TASK_FINISHED' : task.statusCode); + taskDone[id] = wasDone ? true : f.progress; + } + } + } +</script>
\ No newline at end of file diff --git a/modules-available/minilinux/templates/download.html b/modules-available/minilinux/templates/download.html deleted file mode 100644 index 2e32df5a..00000000 --- a/modules-available/minilinux/templates/download.html +++ /dev/null @@ -1 +0,0 @@ -<div data-tm-id="{{task}}" data-tm-log="error" data-tm-progress="progress">{{name}}</div>
\ No newline at end of file diff --git a/modules-available/minilinux/templates/filelist.html b/modules-available/minilinux/templates/filelist.html index 234b6c41..2c26edf9 100644 --- a/modules-available/minilinux/templates/filelist.html +++ b/modules-available/minilinux/templates/filelist.html @@ -1,74 +1,53 @@ - {{#systems}} - <h1>{{title}}</h1> - <div id="download-{{id}}"> - <div class="input-group pull-right" style="max-width: 400px"> - <span class="input-group-addon slx-ga">{{lang_desiredVersion}}</span> - <select id="versionbox" class="form-control"> - {{#versions}} - {{#selected}} - <option value="{{version}}" selected>{{version}}</option> - {{/selected}} - {{^selected}} - <option value="{{version}}">{{version}}</option> - {{/selected}} - {{/versions}} - </select> +<div class="text-right"> + <form method="post" action="?do=minilinux"> + <input type="hidden" name="token" value="{{token}}"> + <input type="hidden" name="version" value="{{versionid}}"> + {{#verify_button}} + <button type="button" class="btn btn-default btn-xs btn-verify" data-version="{{versionid}}" title="{{lang_verifyToolTip}}"> + <span class="glyphicon glyphicon-search"></span> + {{lang_verify}} + </button> + {{/verify_button}} + {{#download_button}} + <button type="button" class="btn btn-xs btn-success btn-download" data-version="{{versionid}}"> + <span class="glyphicon glyphicon-download"></span> + {{lang_download}} + </button> + {{/download_button}} + {{#delete_button}} + <button type="submit" name="show" value="delete" class="btn btn-xs btn-danger" + data-confirm="{{lang_confirmDeleteVersion}}"> + <span class="glyphicon glyphicon-trash"></span> + {{lang_delete}} + </button> + {{/delete_button}} + </form> +</div> +<div class="clearfix slx-smallspace"></div> +<table class="slx-table" style="width:100%"> +{{#files}} + <tr> + <td class="text-nowrap"> </td> + <td class="text-nowrap">{{name}}</td> + <td class="text-nowrap text-right">{{size_s}}</td> + <td class="text-nowrap text-right">{{mtime_s}}</td> + <td style="width:100%"> + {{^dltask}} + {{state}} + {{/dltask}} + {{#dltask}} + <div id="{{fileid}}"> + <div class="progress" style="margin-bottom:0"> + <div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" + style="width:0"></div> + </div> </div> - {{#systemChanged}} - <p> - {{lang_canUpdate1}} <b>{{title}}</b> {{lang_canUpdate2}} - </p> - <button {{perms.update.disabled}} class="btn btn-primary" onclick="slxUpdateAll(this, 'download-{{id}}')"><span class="glyphicon glyphicon-refresh"></span> {{lang_updateAll}}<span></span></button> - {{/systemChanged}} - {{^systemChanged}} - <p>{{lang_systemUpdated}}</p> - {{/systemChanged}} - <hr> - <p><b>{{lang_filesInVersion}} {{version}}</b></p> - <ul class="list-group"> - {{#files}} - <li class="list-group-item" id="{{uid}}"> - <div class="row"> - <div class="col-sm-2">{{name}}</div> - <div class="col-xs-2"> - {{^fileChanged}}<span class="glyphicon glyphicon-ok"></span> <b>{{lang_uptodate}}</b>{{/fileChanged}} - {{#fileChanged}}<span class="glyphicon glyphicon-exclamation-sign"></span> <b>{{lang_outdated}}</b>{{/fileChanged}} - </div> - <div class="col-xs-2"> - {{#fileChanged}}<button {{perms.update.disabled}} class="btn btn-primary btn-xs update-button" onclick="slxUpdate('{{uid}}', '{{id}}', '{{name}}')"><span class="glyphicon glyphicon-refresh"></span> {{lang_update}}</button> {{/fileChanged}} - {{^fileChanged}}<button {{perms.update.disabled}} class="btn btn-default btn-xs" onclick="slxUpdate('{{uid}}', '{{id}}', '{{name}}')"><span class="glyphicon glyphicon-download-alt"></span> {{lang_redownload}}</button> {{/fileChanged}} - </div> - </div> - {{{download}}} - </li> - {{/files}} - </ul> - </div> - </div> - {{/systems}} - {{^systems}} - <div class="row well well-sm">{{lang_configurationPackageNotFound}}</div> - {{/systems}} - -<script type="text/javascript"> -function slxUpdate(uid, id, name) -{ - $('#' + uid).html(''); - $('#' + uid).load('?do=MiniLinux', - { action : "download", token : TOKEN, id : id, name : name, version : $('#versionbox').val() }, - function(response, status, xhr) { - if (status === "error") { - var msg = "Fehler beim Abruf: "; - $('#' + uid).html(msg + xhr.status + " " + xhr.statusText); - } else { - setTimeout(tmInit, 100); - } - }); -} -function slxUpdateAll(t, uid) -{ - $(t).hide(0); - $('#' + uid).find('.update-button').click(); -} -tmInit(); -</script> + {{/dltask}} + </td> + </tr> +{{/files}} +</table> +{{#dltask}} +<div class="hidden" data-tm-id="{{dltask}}" data-tm-callback="dlTmCb"></div> +{{/dltask}} +<div class="slx-space"></div>
\ No newline at end of file diff --git a/modules-available/minilinux/templates/page-minilinux.html b/modules-available/minilinux/templates/page-minilinux.html index afccf230..2cbde608 100644 --- a/modules-available/minilinux/templates/page-minilinux.html +++ b/modules-available/minilinux/templates/page-minilinux.html @@ -1,25 +1,3 @@ -<div id="systemlist"> - <div class="panel panel-default">{{lang_listObtained}}</div> -</div> +<h1>{{lang_minilinuxHeading}}</h1> -<script type="text/javascript"><!-- - document.addEventListener('DOMContentLoaded', function() { - var $list = $('#systemlist'); - function loadSystemList(version) { - $list.addClass('disabled'); - $list.find('select, input, button').prop('disabled', true); - $list.find('div, p, span').addClass('text-muted'); - $list.load('{{{listurl}}}', { token: TOKEN, version: version }, function( response, status, xhr ) { - if ( status === "error" ) { - var msg = "{{lang_errorGetting}}"; - $list.html( msg + xhr.status + " " + xhr.statusText ); - } - $list.removeClass('disabled'); - $('#versionbox').change(function() { - loadSystemList($('#versionbox').val()); - }); - }); - } - loadSystemList(0); - }); -// --></script>
\ No newline at end of file +<p>{{lang_introText}}</p>
\ No newline at end of file diff --git a/modules-available/minilinux/templates/sources.html b/modules-available/minilinux/templates/sources.html new file mode 100644 index 00000000..f2e54745 --- /dev/null +++ b/modules-available/minilinux/templates/sources.html @@ -0,0 +1,42 @@ +<div class="panel panel-default"> + <div class="panel-heading"> + {{lang_sources}} + </div> + <table class="table table-condensed"> + <thead> + <tr> + <th>{{lang_id}}</th> + <th>{{lang_title}}</th> + <th>{{lang_url}}</th> + <th>{{lang_lastUpdate}}</th> + <th>{{lang_key}}</th> + </tr> + </thead> + <tbody> + {{#list}} + <tr> + <td class="small">{{sourceid}}</td> + <td>{{title}}</td> + <td class="small">{{url}}</td> + <td class="{{update_class}}">{{lastupdate_s}}</td> + <td class="text-center"> + <button type="button" class="btn btn-default btn-xs" data-confirm="#confirm-{{source}}" data-close="{{lang_close}}"> + <span class="glyphicon glyphicon-eye-open"></span> + </button> + <pre id="confirm-{{source}}" class="hidden">{{pubkey}}</pre> + </td> + </tr> + {{/list}} + </tbody> + </table> + <div class="panel-body text-right"> + <form method="post" action="?do=minilinux"> + <input type="hidden" name="token" value="{{token}}"> + <button type="submit" name="show" value="updatesources" class="btn btn-default" + onclick="$(this).find('.glyphicon').addClass('slx-rotation')" {{^show_refresh}}disabled{{/show_refresh}}> + <span class="glyphicon glyphicon-refresh"></span> + {{lang_updateSourcesButton}} + </button> + </form> + </div> +</div>
\ No newline at end of file diff --git a/modules-available/minilinux/templates/versionlist.html b/modules-available/minilinux/templates/versionlist.html new file mode 100644 index 00000000..1e5c7c96 --- /dev/null +++ b/modules-available/minilinux/templates/versionlist.html @@ -0,0 +1,39 @@ +<table class="table table-striped"> +<tr> + <th>{{lang_version}}</th> + <th>{{lang_releaseDate}}</th> + <th>{{lang_title}}</th> + <th class="slx-smallcol"></th> + <th class="slx-smallcol"></th> +</tr> +{{#versions}} +<tr> + <td> + <a href="#" class="version-link" data-version="{{versionid}}" {{#downloading}}data-autoclick="true"{{/downloading}}> + {{versionid}} + <b class="caret"></b> + </a> + </td> + <td>{{dateline_s}}</td> + <td>{{title}}</td> + <td> + {{^installed}} + {{^downloading}} + <button type="button" class="btn btn-xs btn-success btn-download" data-version="{{versionid}}"> + <span class="glyphicon glyphicon-download"></span> + {{lang_download}} + </button> + {{/downloading}} + {{/installed}} + </td> + <td> + {{#orphan}} + {{lang_orphanedVersion}} + {{/orphan}} + </td> +</tr> +<tr> + <td colspan="5" class="version-container collapse" data-version="{{versionid}}"></td> +</tr> +{{/versions}} +</table>
\ No newline at end of file |