Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e2f4e9f30a | |||
| 44011afd14 | |||
| cebaa71ce1 | |||
| 0ed9105a05 | |||
| 69ecbe5ad7 | |||
| a218761e99 | |||
| 71d167d5fb | |||
| aabdea8627 | |||
| f220a1384c | |||
| e438ade08e | |||
| ed1d537f60 | |||
| d59bc05f12 | |||
| 4608301f1c | |||
| a865320e3a | |||
| bc8c01900b | |||
| 9704eefc21 | |||
| 52cd52d83c | |||
| 4a29371907 | |||
| 1e5e4e3189 | |||
| 041f7da59b | |||
| 4dae3447d6 | |||
| 7391af6f08 | |||
| 8a640c8219 | |||
| 2857582f46 | |||
| 1d80f03c38 | |||
| d7c20048fe | |||
| cbbdb77a6e | |||
| 2ff995aa95 | |||
| 21705a0e96 | |||
| c03da3be54 | |||
| 69f48ed11a | |||
| caa0c342a4 | |||
| 01b4388b3c | |||
| b870f98ec2 | |||
| a5249102f2 | |||
| 5aa0c57a74 | |||
| 053b076af0 | |||
| 247309e11b | |||
| c9fe08e7b7 | |||
| 468d4dd9b0 | |||
| 6056ba6475 | |||
| 4f03a6fb58 | |||
| d8aa4bc5e4 |
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 13
|
||||
},
|
||||
"rules": {
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"no-console": "off"
|
||||
}
|
||||
}
|
||||
@@ -2818,3 +2818,15 @@
|
||||
* logs: fix recursion when displaying box logs
|
||||
* frontend: fix clear view in logs viewer
|
||||
* dashboard: support links/markdown in checklist items
|
||||
|
||||
[8.0.4]
|
||||
* ami: IMDv2 support
|
||||
* ionos: add contract-owned eu-central-3
|
||||
* dashboard: remove mailbox import/export feature
|
||||
* backupcleaner: do not remove the backup in progress
|
||||
* backups: make noop upload work again
|
||||
* volumes: `/mnt/volumes` is reserved
|
||||
* apps: do not log app logs to output
|
||||
* sftp: restore mode and owner
|
||||
* dashboard: also render checklist items in apps.html
|
||||
|
||||
|
||||
@@ -240,9 +240,10 @@ const ENDPOINTS_OVH = [
|
||||
|
||||
// https://docs.ionos.com/cloud/managed-services/s3-object-storage/endpoints
|
||||
const REGIONS_IONOS = [
|
||||
{ name: 'Frankfurt (DE)', value: 'https://s3-de-central.profitbricks.com', region: 's3-de-central' }, // default
|
||||
{ name: 'Berlin (eu-central-2)', value: 'https://s3-eu-central-2.ionoscloud.com', region: 'eu-central-2' }, // default
|
||||
{ name: 'Logrono (eu-south-2)', value: 'https://s3-eu-south-2.ionoscloud.com', region: 'eu-south-2' }, // default
|
||||
{ name: 'Berlin (eu-central-3)', value: 'https://s3.eu-central-3.ionoscloud.com', region: 'de' }, // default. contract-owned
|
||||
{ name: 'Frankfurt (DE)', value: 'https://s3.eu-central-1.ionoscloud.com', region: 'de' },
|
||||
{ name: 'Berlin (eu-central-2)', value: 'https://s3-eu-central-2.ionoscloud.com', region: 'eu-central-2' },
|
||||
{ name: 'Logrono (eu-south-2)', value: 'https://s3-eu-south-2.ionoscloud.com', region: 'eu-south-2' },
|
||||
];
|
||||
|
||||
// this is not used anywhere because upcloud needs endpoint URL. we detect region from the URL (https://upcloud.com/data-centres)
|
||||
@@ -468,8 +469,6 @@ function redirectIfNeeded(status, currentView) {
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(status, currentView);
|
||||
|
||||
if (status.activated) {
|
||||
console.log('Already activated');
|
||||
if (currentView === 'dashboard') {
|
||||
|
||||
@@ -870,7 +870,7 @@
|
||||
},
|
||||
"timezone": {
|
||||
"title": "Time Zone",
|
||||
"description": "The current timezone setting is <b>{{ timeZone }}</b>.\nThis setting is used for scheduling backup and update tasks."
|
||||
"description": "The current timezone setting is <b>{{ timeZone }}</b>. This setting is used for scheduling backup and update tasks. Timestamps in the UI are always displayed using the browser's timezone."
|
||||
},
|
||||
"updates": {
|
||||
"title": "Updates",
|
||||
|
||||
@@ -1038,7 +1038,7 @@
|
||||
"packageVersion": "Pakketversie",
|
||||
"lastUpdated": "Laatst geüpdatet",
|
||||
"checkForUpdatesAction": "Controleer op updates",
|
||||
"customAppUpdateInfo": "Er zijn geen updates beschikbaar voor deze maatwerk app",
|
||||
"customAppUpdateInfo": "Auto-update is niet beschikbaar voor maatwerk apps.",
|
||||
"updateAvailableAction": "Update beschikbaar",
|
||||
"repository": "Pakket Opslagplaats",
|
||||
"installedAt": "Geïnstalleerd op"
|
||||
@@ -1047,9 +1047,9 @@
|
||||
"title": "Automatische updates",
|
||||
"enabled": "Automatische updates zijn momenteel ingeschakeld.",
|
||||
"disabled": "Automatische updates zijn momenteel uitgeschakeld.",
|
||||
"disableAction": "Uitschakelen",
|
||||
"enableAction": "Inschakelen",
|
||||
"description": "Cloudron controleert de App Store periodiek op updates. Als je dit uitschakelt zorg er dan voor dat je updates handmatig installeert."
|
||||
"disableAction": "Auto-update uitschakelen",
|
||||
"enableAction": "Auto-update inschakelen",
|
||||
"description": "Cloudron controleert periodiek de <a href=\"{{ appStoreLink }}\" target=\"_blank\">App Store</a> op updates."
|
||||
},
|
||||
"noUpdates": "Geen nieuwe updates beschikbaar"
|
||||
},
|
||||
@@ -1492,7 +1492,8 @@
|
||||
"upload": {
|
||||
"title": "Uploaden bestand naar {{ name }}"
|
||||
},
|
||||
"uploadToTmp": "Upload naar /tmp"
|
||||
"uploadToTmp": "Upload naar /tmp",
|
||||
"uploadTo": "Upload naar {{ path }}"
|
||||
},
|
||||
"filemanager": {
|
||||
"title": "Bestandsbeheer",
|
||||
|
||||
@@ -632,7 +632,7 @@
|
||||
<label class="control-label" for="inputPortInfo{{env}}"><input type="checkbox" ng-model="clone.portsEnabled[env]">
|
||||
{{ info.title }}
|
||||
<sup>
|
||||
<a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{info.description}} ({{ HOST_PORT_MIN }} - {{ HOST_PORT_MAX }})"><i class="fa fa-question-circle"></i></a>
|
||||
<a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{info.description}}"><i class="fa fa-question-circle"></i></a>
|
||||
</sup>
|
||||
<small style="padding-left: 5px;" ng-show="info.readOnly">{{ 'appstore.installDialog.portReadOnly' | tr }}</small>
|
||||
</label>
|
||||
@@ -938,7 +938,7 @@
|
||||
<label class="control-label" style="width: 100%" for="locationPortInput{{env}}"><input type="checkbox" ng-model="location.portsEnabled[env]">
|
||||
{{ info.title }}
|
||||
<sup>
|
||||
<a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{info.description}}. {{info.portCount >=1 ? (info.portCount + ' ports. ') : ''}} ({{ HOST_PORT_MIN }} - {{ HOST_PORT_MAX }})"><i class="fa fa-question-circle"></i></a>
|
||||
<a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{info.description}}. {{info.portCount >=1 ? (info.portCount + ' ports. ') : ''}}"><i class="fa fa-question-circle"></i></a>
|
||||
</sup>
|
||||
<small style="padding-left: 5px;" ng-show="info.readOnly">{{ 'appstore.installDialog.portReadOnly' | tr }}</small>
|
||||
<span ng-show="info.portCount" style="display: block; float: right">{{ location.ports[env] }} to {{ location.ports[env] + info.portCount - 1 }} ({{ info.portCount }} ports)</span>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<div ng-show="appPostInstallConfirm.app.manifest.documentationUrl" ng-bind-html="'app.appInfo.appDocsUrl' | tr:{ docsUrl: appPostInstallConfirm.app.manifest.documentationUrl, title: appPostInstallConfirm.app.manifest.title, forumUrl: (appPostInstallConfirm.app.manifest.forumUrl || 'https://forum.cloudron.io') }"></div>
|
||||
<div ng-repeat="item in appPostInstallConfirm.app.checklist">
|
||||
<div class="checklist-item" ng-hide="item.acknowledged">
|
||||
{{ item.message }}
|
||||
<span ng-bind-html="item.message | markdown2html"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
<label class="control-label" for="inputPortInfo{{env}}"><input type="checkbox" ng-model="appInstall.portsEnabled[env]">
|
||||
{{ info.title }}
|
||||
<sup>
|
||||
<a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{info.description}}. {{info.portCount >=1 ? (info.portCount + ' ports. ') : ''}} ({{ HOST_PORT_MIN }} - {{ HOST_PORT_MAX }})"><i class="fa fa-question-circle"></i></a>
|
||||
<a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{info.description}}. {{info.portCount >=1 ? (info.portCount + ' ports. ') : ''}}"><i class="fa fa-question-circle"></i></a>
|
||||
</sup>
|
||||
<small style="padding-left: 5px;" ng-show="info.readOnly">{{ 'appstore.installDialog.portReadOnly' | tr }}</small>
|
||||
</label>
|
||||
@@ -407,7 +407,7 @@
|
||||
<center>
|
||||
<a href="" ng-click="appstoreLogin.setupType = 'signup'" ng-show="appstoreLogin.setupType === 'login'">{{ 'appstore.accountDialog.switchToSignUpAction' | tr }}</a>
|
||||
<a href="" ng-click="appstoreLogin.setupType = 'login'" ng-show="appstoreLogin.setupType === 'signup' || appstoreLogin.setupType === 'setupToken'">{{ 'appstore.accountDialog.switchToLoginAction' | tr }}</a>
|
||||
<span ng-show="appstoreLogin.setupType !== 'setupToken'"> or <a href="" ng-click="appstoreLogin.setupType = 'setupToken'">use a setup token</a></span>
|
||||
<span ng-show="appstoreLogin.setupType !== 'setupToken'"> or <a href="" ng-click="appstoreLogin.setupType = 'setupToken'">Use a setup token</a></span>
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
angular.module('Application').controller('AppStoreController', ['$scope', '$translate', '$location', '$timeout', '$routeParams', 'Client', function ($scope, $translate, $location, $timeout, $routeParams, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.HOST_PORT_MIN = 1024;
|
||||
$scope.HOST_PORT_MIN = 1;
|
||||
$scope.HOST_PORT_MAX = 65535;
|
||||
|
||||
$scope.ready = false;
|
||||
|
||||
@@ -476,7 +476,7 @@
|
||||
<span>{{ prettyProviderName(backupConfig.provider) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row" ng-show="backupConfig.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'backups.location.location' | tr }}</span>
|
||||
</div>
|
||||
|
||||
+52
-107
@@ -108,79 +108,74 @@
|
||||
<h4 class="modal-title">{{ 'email.editMailboxDialog.title' | tr:{ name: mailboxes.edit.name, domain: domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="mailboxedit_form" role="form" ng-submit="mailboxes.edit.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'email.editMailboxDialog.owner' | tr }}</label>
|
||||
<div class="control-label">
|
||||
<multiselect ng-model="mailboxes.edit.owner" options="o.display for o in owners" data-compare-by="name" data-header-key="header" data-divider-key="divider" data-multiple="false" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'email.editMailboxDialog.owner' | tr }}</label>
|
||||
<div class="control-label">
|
||||
<multiselect ng-model="mailboxes.edit.owner" options="o.display for o in owners" data-compare-by="name" data-header-key="header" data-divider-key="divider" data-multiple="false" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group aliases">
|
||||
<label class="control-label">{{ 'email.editMailboxDialog.aliases' | tr }}</label>
|
||||
<div class="has-error" ng-show="mailboxes.edit.error">{{ mailboxes.edit.error.message }}</div>
|
||||
<div class="form-group aliases">
|
||||
<label class="control-label">{{ 'email.editMailboxDialog.aliases' | tr }}</label>
|
||||
<div class="has-error" ng-show="mailboxes.edit.error">{{ mailboxes.edit.error.message }}</div>
|
||||
|
||||
<div class="row" ng-repeat="alias in mailboxes.edit.aliases | orderBy:'reversedSortingNotation'">
|
||||
<div class="col col-lg-11">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control input-sm" ng-model="alias.name" autofocus>
|
||||
<div class="row" ng-repeat="alias in mailboxes.edit.aliases | orderBy:'reversedSortingNotation'">
|
||||
<div class="col col-lg-11">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control input-sm" ng-model="alias.name">
|
||||
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown">
|
||||
<span>@{{ alias.domain }}</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li ng-repeat="incomingDomain in incomingDomains">
|
||||
<a href="" ng-click="alias.domain = incomingDomain.domain">{{ incomingDomain.domain }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown">
|
||||
<span>@{{ alias.domain }}</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li ng-repeat="incomingDomain in incomingDomains">
|
||||
<a href="" ng-click="alias.domain = incomingDomain.domain">{{ incomingDomain.domain }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-lg-1">
|
||||
<button class="btn btn-danger btn-sm" ng-click="mailboxes.edit.delAlias($event, alias)"><i class="far fa-trash-alt"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="mailboxes.edit.aliases.length === 0">
|
||||
{{ 'email.editMailboxDialog.noAliases' | tr }} <a href="" ng-click="mailboxes.edit.addAlias($event)">{{ 'email.editMailboxDialog.addAliasAction' | tr }}</a>
|
||||
</div>
|
||||
<div ng-show="mailboxes.edit.aliases.length > 0" style="margin-top: 5px;">
|
||||
<a href="" ng-click="mailboxes.edit.addAlias($event)">{{ 'email.editMailboxDialog.addAnotherAliasAction' | tr }}</a>
|
||||
<div class="col col-lg-1">
|
||||
<button class="btn btn-danger btn-sm" ng-click="mailboxes.edit.delAlias($event, alias)"><i class="far fa-trash-alt"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="storageQuota">
|
||||
<input id="storageQuota" type="checkbox" ng-model="mailboxes.edit.storageQuotaEnabled">
|
||||
{{ 'email.editMailboxDialog.enableStorageQuota' | tr }} <b ng-hide="!mailboxes.edit.storageQuotaEnabled">: {{ mailboxes.edit.storageQuota | prettyDecimalSize }}</b>
|
||||
</input>
|
||||
</label>
|
||||
<input type="range" id="storageQuota" ng-disabled="!mailboxes.edit.storageQuotaEnabled" ng-model="mailboxes.edit.storageQuota" step="500000000" min="{{ storageQuotaTicks[0] }}" max="{{ storageQuotaTicks[storageQuotaTicks.length-1] }}" list="storageQuotaTicks" />
|
||||
<datalist id="storageQuotaTicks">
|
||||
<option ng-repeat="quota in storageQuotaTicks" value="{{ quota }}"></option>
|
||||
</datalist>
|
||||
<div ng-show="mailboxes.edit.aliases.length === 0">
|
||||
{{ 'email.editMailboxDialog.noAliases' | tr }} <a href="" ng-click="mailboxes.edit.addAlias($event)">{{ 'email.editMailboxDialog.addAliasAction' | tr }}</a>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailboxes.edit.enablePop3"> {{ 'email.updateMailboxDialog.enablePop3' | tr }}</input>
|
||||
</label>
|
||||
<div ng-show="mailboxes.edit.aliases.length > 0" style="margin-top: 5px;">
|
||||
<a href="" ng-click="mailboxes.edit.addAlias($event)">{{ 'email.editMailboxDialog.addAnotherAliasAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailboxes.edit.active"> {{ 'email.updateMailboxDialog.activeCheckbox' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="storageQuota">
|
||||
<input id="storageQuota" type="checkbox" ng-model="mailboxes.edit.storageQuotaEnabled">
|
||||
{{ 'email.editMailboxDialog.enableStorageQuota' | tr }} <b ng-hide="!mailboxes.edit.storageQuotaEnabled">: {{ mailboxes.edit.storageQuota | prettyDecimalSize }}</b>
|
||||
</input>
|
||||
</label>
|
||||
<input type="range" id="storageQuota" ng-disabled="!mailboxes.edit.storageQuotaEnabled" ng-model="mailboxes.edit.storageQuota" step="500000000" min="{{ storageQuotaTicks[0] }}" max="{{ storageQuotaTicks[storageQuotaTicks.length-1] }}" list="storageQuotaTicks" />
|
||||
<datalist id="storageQuotaTicks">
|
||||
<option ng-repeat="quota in storageQuotaTicks" value="{{ quota }}"></option>
|
||||
</datalist>
|
||||
</div>
|
||||
|
||||
<input class="hide" type="submit" ng-disabled="mailboxedit_form.$invalid || mailboxes.edit.busy || !mailboxes.edit.owner"/>
|
||||
</form>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailboxes.edit.enablePop3"> {{ 'email.updateMailboxDialog.enablePop3' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailboxes.edit.active"> {{ 'email.updateMailboxDialog.activeCheckbox' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="mailboxes.edit.submit()" ng-disabled="mailboxedit_form.$invalid || mailboxes.edit.busy || !mailboxes.edit.owner"><i class="fa fa-circle-notch fa-spin" ng-show="mailboxes.edit.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="mailboxes.edit.submit()" ng-disabled="mailboxes.edit.busy || !mailboxes.edit.owner"><i class="fa fa-circle-notch fa-spin" ng-show="mailboxes.edit.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,44 +205,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal import mailboxes -->
|
||||
<div class="modal fade" id="mailboxImportModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.mailboxImportDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-show="!mailboxImport.done">
|
||||
<div ng-show="!mailboxImport.busy">
|
||||
<p ng-bind-html=" 'email.mailboxImportDialog.description' | tr:{ docsLink: 'https://cloudron.io/documentation/email/#import-mailboxes' } "></p>
|
||||
<input type="file" style="display: none;" id="mailboxImportFileInput" accept="application/json,text/csv"/>
|
||||
<button class="btn btn-primary" ng-click="mailboxImport.openFileInput()">{{ 'email.mailboxImportDialog.fileInput' | tr }}</button>
|
||||
<br/>
|
||||
<br/>
|
||||
<p class="text-danger" ng-show="mailboxImport.error.file">{{ mailboxImport.error.file }}</p>
|
||||
<p class="text-info" ng-show="mailboxImport.mailboxes.length">{{ 'email.mailboxImportDialog.mailboxesFound' | tr:{ count: mailboxImport.mailboxes.length } }}</p>
|
||||
</div>
|
||||
<div ng-show="mailboxImport.busy" class="progress progress-striped active">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ mailboxImport.percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="mailboxImport.done">
|
||||
<p>{{ 'email.mailboxImportDialog.success' | tr:{ count: mailboxImport.success } }}</p>
|
||||
<div ng-show="mailboxImport.error.import.length">
|
||||
<p class="text-danger">{{ 'email.mailboxImportDialog.failed' | tr }}</p>
|
||||
<div ng-repeat="tmp in mailboxImport.error.import"><b>{{ tmp.mailbox.name }}@{{ tmp.mailbox.domain }}:</b> {{ tmp.error.message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="mailboxImport.import()" ng-show="!mailboxImport.done" ng-disabled="mailboxImport.busy || !mailboxImport.mailboxes.length"><i class="fa fa-circle-notch fa-spin" ng-show="mailboxImport.busy"></i> {{ 'email.mailboxImportDialog.importAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal add mailinglist -->
|
||||
<div class="modal fade" id="mailinglistAddModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
@@ -417,18 +374,6 @@
|
||||
<div class="text-left">
|
||||
<h3 style="margin-bottom: 15px;">{{ 'email.incoming.mailboxes.title' | tr }}
|
||||
<button class="btn btn-primary btn-outline pull-right" ng-click="mailboxes.add.show()" ng-disabled="!domain.mailConfig.enabled" tooltip-enable="!domain.mailConfig.enabled" uib-tooltip="{{ 'email.incoming.mailboxes.disabledTooltip' | tr }}"><i class="fa fa-inbox"></i> {{ 'email.incoming.mailboxes.addAction' | tr }}</button>
|
||||
<div class="btn-group pull-right" style="margin-left: 5px;">
|
||||
<button class="btn btn-default" ng-click="mailboxImport.show()" uib-tooltip="{{ 'email.incoming.mailboxes.importTooltip' | tr }}" tooltip-append-to-body="true"><i class="fas fa-download"></i></button>
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown" uib-tooltip="{{ 'email.incoming.mailboxes.exportTooltip' | tr }}" tooltip-append-to-body="true">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a href="" ng-click="mailboxExport('csv')">{{ 'email.incoming.mailboxes.mailboxExport.csv' | tr }}</a></li>
|
||||
<li><a href="" ng-click="mailboxExport('json')">{{ 'email.incoming.mailboxes.mailboxExport.json' | tr }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<input class="form-control pull-right" style="width: 200px;" placeholder="{{ 'main.searchPlaceholder' | tr }}" type="text" ng-model="mailboxes.search" ng-model-options="{ debounce: 1000 }" ng-change="mailboxes.updateFilter()" />
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -377,172 +377,6 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
}
|
||||
};
|
||||
|
||||
$scope.mailboxImport = {
|
||||
busy: false,
|
||||
done: false,
|
||||
error: null,
|
||||
percent: 0,
|
||||
success: 0,
|
||||
mailboxes: [],
|
||||
|
||||
reset: function () {
|
||||
$scope.mailboxImport.busy = false;
|
||||
$scope.mailboxImport.error = null;
|
||||
$scope.mailboxImport.mailboxes = [];
|
||||
$scope.mailboxImport.percent = 0;
|
||||
$scope.mailboxImport.success = 0;
|
||||
$scope.mailboxImport.done = false;
|
||||
},
|
||||
|
||||
handleFileChanged: function () {
|
||||
$scope.mailboxImport.reset();
|
||||
|
||||
var fileInput = document.getElementById('mailboxImportFileInput');
|
||||
if (!fileInput.files || !fileInput.files[0]) return;
|
||||
|
||||
var file = fileInput.files[0];
|
||||
if (file.type !== 'application/json' && file.type !== 'text/csv') return console.log('Unsupported file type.');
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', function () {
|
||||
$scope.$apply(function () {
|
||||
$scope.mailboxImport.mailboxes = [];
|
||||
var mailboxes = [];
|
||||
|
||||
if (file.type === 'text/csv') {
|
||||
var lines = reader.result.split('\n');
|
||||
if (lines.length === 0) return $scope.mailboxImport.error = { file: 'Imported file has no lines' };
|
||||
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i].trim();
|
||||
if (!line) continue;
|
||||
var items = line.split(',');
|
||||
if (items.length !== 4) {
|
||||
$scope.mailboxImport.error = { file: 'Line ' + (i+1) + ' has wrong column count. Expecting 4' };
|
||||
return;
|
||||
}
|
||||
mailboxes.push({
|
||||
name: items[0].trim(),
|
||||
domain: items[1].trim(),
|
||||
owner: items[2].trim(),
|
||||
ownerType: items[3].trim(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
mailboxes = JSON.parse(reader.result).map(function (mailbox) {
|
||||
return {
|
||||
name: mailbox.name,
|
||||
domain: mailbox.domain,
|
||||
owner: mailbox.owner,
|
||||
ownerType: mailbox.ownerType
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to parse mailboxes.', e);
|
||||
$scope.mailboxImport.error = { file: 'Imported file is not valid JSON' };
|
||||
}
|
||||
}
|
||||
|
||||
$scope.mailboxImport.mailboxes = mailboxes;
|
||||
});
|
||||
}, false);
|
||||
reader.readAsText(file);
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.mailboxImport.reset();
|
||||
|
||||
// named so no duplactes
|
||||
document.getElementById('mailboxImportFileInput').addEventListener('change', $scope.mailboxImport.handleFileChanged);
|
||||
|
||||
$('#mailboxImportModal').modal('show');
|
||||
},
|
||||
|
||||
openFileInput: function () {
|
||||
$('#mailboxImportFileInput').click();
|
||||
},
|
||||
|
||||
import: function () {
|
||||
$scope.mailboxImport.percent = 0;
|
||||
$scope.mailboxImport.success = 0;
|
||||
$scope.mailboxImport.done = false;
|
||||
$scope.mailboxImport.error = { import: [] };
|
||||
$scope.mailboxImport.busy = true;
|
||||
|
||||
var processed = 0;
|
||||
|
||||
async.eachSeries($scope.mailboxImport.mailboxes, function (mailbox, callback) {
|
||||
var owner = $scope.owners.find(function (o) { return o.display === mailbox.owner && o.type === mailbox.ownerType; }); // owner may not exist
|
||||
if (!owner) {
|
||||
$scope.mailboxImport.error.import.push({ error: new Error('Could not detect owner'), mailbox: mailbox });
|
||||
++processed;
|
||||
$scope.mailboxImport.percent = 100 * processed / $scope.mailboxImport.mailboxes.length;
|
||||
return callback();
|
||||
}
|
||||
|
||||
Client.addMailbox(mailbox.domain, mailbox.name, owner.id, mailbox.ownerType, function (error) {
|
||||
if (error) $scope.mailboxImport.error.import.push({ error: error, mailbox: mailbox });
|
||||
else ++$scope.mailboxImport.success;
|
||||
|
||||
++processed;
|
||||
$scope.mailboxImport.percent = 100 * processed / $scope.mailboxImport.mailboxes.length;
|
||||
|
||||
callback();
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.mailboxImport.busy = false;
|
||||
$scope.mailboxImport.done = true;
|
||||
if ($scope.mailboxImport.success) $scope.mailboxes.refresh();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.mailboxExport = function (type) {
|
||||
// FIXME only does first 10k mailboxes
|
||||
Client.listMailboxes($scope.domain.domain, '', 1, 10000, function (error, result) {
|
||||
if (error) {
|
||||
Client.error('Failed to list mailboxes. Full error in the webinspector.');
|
||||
return console.error('Failed to list mailboxes.', error);
|
||||
}
|
||||
|
||||
var content = '';
|
||||
|
||||
if (type === 'json') {
|
||||
content = JSON.stringify(result.map(function (mailbox) {
|
||||
var owner = $scope.owners.find(function (o) { return o.id === mailbox.ownerId; }); // owner may not exist
|
||||
|
||||
return {
|
||||
name: mailbox.name,
|
||||
domain: mailbox.domain,
|
||||
owner: owner ? owner.display : '', // this meta property is set when we get the user list
|
||||
ownerType: owner ? owner.type : '',
|
||||
active: mailbox.active,
|
||||
aliases: mailbox.aliases
|
||||
};
|
||||
}), null, 2);
|
||||
} else if (type === 'csv') {
|
||||
content = result.map(function (mailbox) {
|
||||
var owner = $scope.owners.find(function (o) { return o.id === mailbox.ownerId; }); // owner may not exist
|
||||
|
||||
var aliases = mailbox.aliases.map(function (a) { return a.name + '@' + a.domain; }).join(' ');
|
||||
return [ mailbox.name, mailbox.domain, owner ? owner.display : '', owner ? owner.type : '', aliases, mailbox.active ].join(',');
|
||||
}).join('\n');
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
var file = new Blob([ content ], { type: type === 'json' ? 'application/json' : 'text/csv' });
|
||||
var a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(file);
|
||||
a.download = $scope.domain.domain.replaceAll('.','_') + '-mailboxes.' + type;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.mailboxes = {
|
||||
mailboxes: [],
|
||||
search: '',
|
||||
|
||||
@@ -292,48 +292,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal user import -->
|
||||
<div class="modal fade" id="userImportModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'users.userImportDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-show="!userImport.done">
|
||||
<div ng-show="!userImport.busy">
|
||||
<p ng-bind-html=" 'users.userImportDialog.description' | tr:{ docsLink: 'https://docs.cloudron.io/user-management/#import-users' } "></p>
|
||||
<input type="file" style="display: none;" id="userImportFileInput" accept="application/json,text/csv"/>
|
||||
<button class="btn btn-primary" ng-click="userImport.openFileInput()">{{ 'users.userImportDialog.fileInput' | tr }}</button>
|
||||
<br/>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="userImport.sendInvite" id="inputUserImportSendInvite"> {{ 'users.userImportDialog.sendInviteCheckbox' | tr }}
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-danger" ng-show="userImport.error.file">{{ userImport.error.file }}</p>
|
||||
<p class="text-info" ng-show="userImport.users.length">{{ 'users.userImportDialog.usersFound' | tr:{ count: userImport.users.length } }}</p>
|
||||
</div>
|
||||
<div ng-show="userImport.busy" class="progress progress-striped active">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ userImport.percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="userImport.done">
|
||||
<p>{{ 'users.userImportDialog.success' | tr:{ count: userImport.success } }}</p>
|
||||
<div ng-show="userImport.error.import.length">
|
||||
<p class="text-danger">{{ 'users.userImportDialog.failed' | tr }}</p>
|
||||
<div ng-repeat="tmp in userImport.error.import"><b>{{ tmp.user.email }}:</b> {{ tmp.error.message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="userImport.import()" ng-show="!userImport.done" ng-disabled="userImport.busy || !userImport.users.length"><i class="fa fa-circle-notch fa-spin" ng-show="userImport.busy"></i> {{ 'users.userImportDialog.importAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal password reset -->
|
||||
<div class="modal fade" id="passwordResetModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
@@ -454,19 +412,6 @@
|
||||
<input type="text" id="userSearchInput" class="form-control" style="max-width: 350px;" ng-model="userSearchString" ng-model-options="{ debounce: 1000 }" ng-change="updateFilter()" placeholder="{{ 'main.searchPlaceholder' | tr }}"/>
|
||||
<multiselect ng-model="userStateFilter" ms-header="{{ 'apps.stateFilterHeader' | tr }}" ms-selected="{{ userStateFilter }}" options="state.label for state in userStates" data-multiple="false"></multiselect>
|
||||
<div style="flex-grow: 1;"></div>
|
||||
<!-- import/export buttons are hidden until we figure what the exact use case is -->
|
||||
<div class="btn-group" ng-hide="true">
|
||||
<button class="btn btn-default" ng-click="userImport.show()" uib-tooltip="{{ 'users.userImport.tooltip' | tr }}" tooltip-append-to-body="true"><i class="fas fa-download"></i></button>
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown" uib-tooltip="{{ 'users.userExport.tooltip' | tr }}" tooltip-append-to-body="true">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a href="" ng-click="userExport('csv')">{{ 'users.userExport.csv' | tr }}</a></li>
|
||||
<li><a href="" ng-click="userExport('json')">{{ 'users.userExport.json' | tr }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-outline" ng-click="userAdd.show()">
|
||||
<i class="fa fa-user-plus"></i> {{ 'users.newUserAction' | tr }}
|
||||
</button>
|
||||
|
||||
@@ -67,171 +67,6 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
return true;
|
||||
};
|
||||
|
||||
$scope.userImport = {
|
||||
busy: false,
|
||||
done: false,
|
||||
error: null,
|
||||
percent: 0,
|
||||
success: 0,
|
||||
users: [],
|
||||
sendInvite: false,
|
||||
|
||||
reset: function () {
|
||||
$scope.userImport.busy = false;
|
||||
$scope.userImport.error = null;
|
||||
$scope.userImport.users = [];
|
||||
$scope.userImport.percent = 0;
|
||||
$scope.userImport.success = 0;
|
||||
$scope.userImport.done = false;
|
||||
$scope.userImport.sendInvite = false;
|
||||
},
|
||||
|
||||
handleFileChanged: function () {
|
||||
$scope.userImport.reset();
|
||||
|
||||
var fileInput = document.getElementById('userImportFileInput');
|
||||
if (!fileInput.files || !fileInput.files[0]) return;
|
||||
|
||||
var file = fileInput.files[0];
|
||||
if (file.type !== 'application/json' && file.type !== 'text/csv') return console.log('Unsupported file type.');
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', function () {
|
||||
$scope.$apply(function () {
|
||||
$scope.userImport.users = [];
|
||||
var users = [];
|
||||
if (file.type === 'text/csv') {
|
||||
var lines = reader.result.split('\n');
|
||||
if (lines.length === 0) return $scope.userImport.error = { file: 'Imported file has no lines' };
|
||||
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i].trim();
|
||||
if (!line) continue;
|
||||
var items = line.split(',');
|
||||
if (items.length !== 5) {
|
||||
$scope.userImport.error = { file: 'Line ' + (i+1) + ' has wrong column count. Expecting 5' };
|
||||
return;
|
||||
}
|
||||
users.push({
|
||||
username: items[0].trim(),
|
||||
email: items[1].trim(),
|
||||
fallbackEmail: items[2].trim(),
|
||||
displayName: items[3].trim(),
|
||||
role: items[4].trim()
|
||||
});
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
users = JSON.parse(reader.result).map(function (user) {
|
||||
return {
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
fallbackEmail: user.fallbackEmail,
|
||||
displayName: user.displayName,
|
||||
role: user.role
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to parse users.', e);
|
||||
$scope.userImport.error = { file: 'Imported file is not valid JSON:' + e.message };
|
||||
}
|
||||
}
|
||||
$scope.userImport.users = users;
|
||||
});
|
||||
}, false);
|
||||
reader.readAsText(file);
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.userImport.reset();
|
||||
|
||||
// named so no duplactes
|
||||
document.getElementById('userImportFileInput').addEventListener('change', $scope.userImport.handleFileChanged);
|
||||
|
||||
$('#userImportModal').modal('show');
|
||||
},
|
||||
|
||||
openFileInput: function () {
|
||||
$('#userImportFileInput').click();
|
||||
},
|
||||
|
||||
import: function () {
|
||||
$scope.userImport.percent = 0;
|
||||
$scope.userImport.success = 0;
|
||||
$scope.userImport.done = false;
|
||||
$scope.userImport.error = { import: [] };
|
||||
$scope.userImport.busy = true;
|
||||
|
||||
var processed = 0;
|
||||
|
||||
async.eachSeries($scope.userImport.users, function (user, callback) {
|
||||
Client.addUser(user, function (error, userId) {
|
||||
if (error) $scope.userImport.error.import.push({ error: error, user: user });
|
||||
else ++$scope.userImport.success;
|
||||
|
||||
++processed;
|
||||
$scope.userImport.percent = 100 * processed / $scope.userImport.users.length;
|
||||
|
||||
if (!error && $scope.userImport.sendInvite) {
|
||||
console.log('sending', userId, user.email);
|
||||
Client.sendInviteEmail(userId, user.email, function (error) {
|
||||
if (error) console.error('Failed to send invite.', error);
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.userImport.busy = false;
|
||||
$scope.userImport.done = true;
|
||||
if ($scope.userImport.success) {
|
||||
refreshCurrentPage();
|
||||
refreshAllUsers();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// supported types are 'json' and 'csv'
|
||||
$scope.userExport = function (type) {
|
||||
Client.getAllUsers(function (error, result) {
|
||||
if (error) {
|
||||
Client.error('Failed to list users. Full error in the webinspector.');
|
||||
return console.error('Failed to list users.', error);
|
||||
}
|
||||
|
||||
var content = '';
|
||||
if (type === 'json') {
|
||||
content = JSON.stringify(result.map(function (user) {
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
fallbackEmail: user.fallbackEmail,
|
||||
displayName: user.displayName,
|
||||
role: user.role,
|
||||
active: user.active
|
||||
};
|
||||
}), null, 2);
|
||||
} else if (type === 'csv') {
|
||||
content = result.map(function (user) {
|
||||
return [ user.id, user.username, user.email, user.fallbackEmail, user.displayName, user.role, user.active ].join(',');
|
||||
}).join('\n');
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
var file = new Blob([ content ], { type: type === 'json' ? 'application/json' : 'text/csv' });
|
||||
var a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(file);
|
||||
a.download = type === 'json' ? 'users.json' : 'users.csv';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.userRemove = {
|
||||
busy: false,
|
||||
error: null,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import globals from 'globals';
|
||||
import js from '@eslint/js';
|
||||
import pluginVue from 'eslint-plugin-vue';
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
...pluginVue.configs['flat/essential'],
|
||||
{
|
||||
files: ["**/*.js"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
ecmaVersion: 13,
|
||||
sourceType: 'module'
|
||||
},
|
||||
rules: {
|
||||
semi: "error",
|
||||
"prefer-const": "error"
|
||||
}
|
||||
}
|
||||
];
|
||||
Generated
+1360
-112
File diff suppressed because it is too large
Load Diff
+11
-8
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "my-vue-app",
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
@@ -16,17 +16,20 @@
|
||||
"anser": "^2.1.1",
|
||||
"combokeys": "^3.0.1",
|
||||
"filesize": "^10.1.4",
|
||||
"marked": "^13.0.2",
|
||||
"marked": "^13.0.3",
|
||||
"moment": "^2.30.1",
|
||||
"pankow": "^1.6.8",
|
||||
"pankow": "^1.7.3",
|
||||
"pankow-viewers": "^1.0.4",
|
||||
"superagent": "^9.0.2",
|
||||
"vue": "^3.4.33",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-router": "^4.4.0"
|
||||
"vue": "^3.4.38",
|
||||
"vue-i18n": "^9.14.0",
|
||||
"vue-router": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"vite": "^5.3.4"
|
||||
"@eslint/js": "^9.9.0",
|
||||
"@vitejs/plugin-vue": "^5.1.2",
|
||||
"eslint": "^9.9.0",
|
||||
"eslint-plugin-vue": "^9.27.0",
|
||||
"vite": "^5.4.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,13 +67,12 @@ export function createDirectoryModel(origin, accessToken, api) {
|
||||
|
||||
return result.body.entries;
|
||||
},
|
||||
async upload(targetDir, file, progressHandler) {
|
||||
upload(targetDir, file, progressHandler) {
|
||||
// file may contain a file name or a file path + file name
|
||||
const relativefilePath = (file.webkitRelativePath ? file.webkitRelativePath : file.name);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
const req = new Promise(function (resolve, reject) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.addEventListener('load', () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
resolve(xhr.response);
|
||||
@@ -94,18 +93,20 @@ export function createDirectoryModel(origin, accessToken, api) {
|
||||
if (event.loaded) progressHandler({ direction: 'upload', loaded: event.loaded});
|
||||
});
|
||||
|
||||
xhr.open('POST', `${origin}/api/v1/${api}/files/${encodeURIComponent(sanitize(targetDir + '/' + relativefilePath))}?access_token=${accessToken}`);
|
||||
xhr.open('POST', `${origin}/api/v1/${api}/files/${encodeURIComponent(sanitize(targetDir + '/' + relativefilePath))}?access_token=${accessToken}&overwrite=true`);
|
||||
|
||||
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
|
||||
xhr.setRequestHeader('Content-Length', file.size);
|
||||
|
||||
xhr.send(file);
|
||||
});
|
||||
|
||||
const res = await req;
|
||||
// attach for upstream xhr.abort()
|
||||
req.xhr = xhr;
|
||||
|
||||
return req;
|
||||
},
|
||||
async newFile(filePath) {
|
||||
await this.save(filePath, '')
|
||||
await this.save(filePath, '');
|
||||
},
|
||||
async newFolder(folderPath) {
|
||||
await superagent.post(`${origin}/api/v1/${api}/files/${folderPath}?access_token=${accessToken}&directory=true`);
|
||||
|
||||
+110
-104
@@ -213,6 +213,76 @@ export default {
|
||||
this.loadCwd();
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.busy = true;
|
||||
const type = this.$route.params.type || 'app';
|
||||
const resourceId = this.$route.params.resourceId;
|
||||
const cwd = this.$route.params.cwd;
|
||||
|
||||
if (type === 'app') {
|
||||
let error, result;
|
||||
try {
|
||||
result = await superagent.get(`${this.apiOrigin}/api/v1/apps/${resourceId}`).query({ access_token: this.accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (error || result.statusCode !== 200) {
|
||||
console.error(`Invalid resource ${type} ${resourceId}`, error || result.statusCode);
|
||||
return this.onFatalError(`Invalid resource ${type} ${resourceId}`);
|
||||
}
|
||||
|
||||
this.appLink = `https://${result.body.fqdn}`;
|
||||
this.title = `${result.body.label || result.body.fqdn} (${result.body.manifest.title})`;
|
||||
} else if (type === 'volume') {
|
||||
let error, result;
|
||||
try {
|
||||
result = await superagent.get(`${this.apiOrigin}/api/v1/volumes/${resourceId}`).query({ access_token: this.accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (error || result.statusCode !== 200) {
|
||||
console.error(`Invalid resource ${type} ${resourceId}`, error || result.statusCode);
|
||||
return this.onFatalError(`Invalid resource ${type} ${resourceId}`);
|
||||
}
|
||||
|
||||
this.title = result.body.name;
|
||||
} else {
|
||||
return this.onFatalError(`Unsupported type ${type}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await superagent.get(`${this.apiOrigin}/api/v1/dashboard/config`).query({ access_token: this.accessToken });
|
||||
this.footerContent = marked.parse(result.body.footer);
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch Cloudron config.', e);
|
||||
}
|
||||
|
||||
window.document.title = `File Manager - ${this.title}`;
|
||||
|
||||
this.cwd = sanitize('/' + (cwd ? cwd.join('/') : '/'));
|
||||
this.resourceType = type;
|
||||
this.resourceId = resourceId;
|
||||
|
||||
this.directoryModel = createDirectoryModel(this.apiOrigin, this.accessToken, type === 'volume' ? `volumes/${resourceId}` : `apps/${resourceId}`);
|
||||
this.ownersModel = this.directoryModel.ownersModel;
|
||||
|
||||
this.loadCwd();
|
||||
|
||||
this.$watch(() => this.$route.params, (toParams, previousParams) => {
|
||||
if (toParams.type !== 'app' && toParams.type !== 'volume') return this.onFatalError(`Unknown type ${toParams.type}`);
|
||||
|
||||
if ((toParams.type !== this.resourceType) || (toParams.resourceId !== this.resourceId)) {
|
||||
this.resourceType = toParams.type;
|
||||
this.resourceId = toParams.resourceId;
|
||||
|
||||
this.directoryModel = createDirectoryModel(this.apiOrigin, this.accessToken, toParams.type === 'volume' ? `volumes/${toParams.resourceId}` : `apps/${toParams.resourceId}`);
|
||||
}
|
||||
|
||||
this.cwd = toParams.cwd ? `/${toParams.cwd.join('/')}` : '/';
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
onFatalError(errorMessage) {
|
||||
this.fatalError = errorMessage;
|
||||
@@ -225,8 +295,8 @@ export default {
|
||||
this.$refs.uploadMenu.open(event, elem);
|
||||
},
|
||||
onCancelUpload() {
|
||||
if (!this.uploadRequest) return;
|
||||
this.uploadRequest.abort();
|
||||
if (!this.uploadRequest || !this.uploadRequest.xhr) return;
|
||||
this.uploadRequest.xhr.abort();
|
||||
},
|
||||
// generic dialog focus handler
|
||||
onDialogShow(focusElementId) {
|
||||
@@ -292,36 +362,50 @@ export default {
|
||||
async onDrop(targetFolder, dataTransfer, files) {
|
||||
const fullTargetFolder = sanitize(this.cwd + '/' + targetFolder);
|
||||
|
||||
// if dataTransfer is set, we have a file/folder drop from outside
|
||||
if (dataTransfer) {
|
||||
// figure if a folder was dropped on a modern browser, in this case the first would have to be a directory
|
||||
let folderItem;
|
||||
try {
|
||||
folderItem = dataTransfer.items[0].webkitGetAsEntry();
|
||||
if (folderItem.isFile) return this.$refs.fileUploader.addFiles(dataTransfer.files, fullTargetFolder, false);
|
||||
} catch (e) {
|
||||
return this.$refs.fileUploader.addFiles(dataTransfer.files, fullTargetFolder, false);
|
||||
|
||||
async function getFile(entry) {
|
||||
return new Promise((resolve, reject) => {
|
||||
entry.file(resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
// if we got here we have a folder drop and a modern browser
|
||||
// now traverse the folder tree and create a file list
|
||||
var that = this;
|
||||
function traverseFileTree(item, path) {
|
||||
async function readEntries(dirReader) {
|
||||
return new Promise((resolve, reject) => {
|
||||
dirReader.readEntries(resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
const fileList = [];
|
||||
async function traverseFileTree(item) {
|
||||
if (item.isFile) {
|
||||
item.file(function (file) {
|
||||
that.$refs.fileUploader.addFiles([file], sanitize(`${that.cwd}/${targetFolder}`), false);
|
||||
});
|
||||
fileList.push(await getFile(item));
|
||||
} else if (item.isDirectory) {
|
||||
// Get folder contents
|
||||
var dirReader = item.createReader();
|
||||
dirReader.readEntries(function (entries) {
|
||||
for (let i in entries) {
|
||||
traverseFileTree(entries[i], item.name);
|
||||
}
|
||||
});
|
||||
const dirReader = item.createReader();
|
||||
const entries = await readEntries(dirReader);
|
||||
|
||||
for (let i in entries) {
|
||||
await traverseFileTree(entries[i], item.name);
|
||||
}
|
||||
} else {
|
||||
console.log('Skipping uknown file type', item);
|
||||
}
|
||||
}
|
||||
|
||||
traverseFileTree(folderItem, '');
|
||||
// collect all files to upload
|
||||
for (const item of dataTransfer.items) {
|
||||
const entry = item.webkitGetAsEntry();
|
||||
|
||||
if (entry.isFile) {
|
||||
fileList.push(await getFile(entry));
|
||||
} else if (entry.isDirectory) {
|
||||
await traverseFileTree(entry, sanitize(`${this.cwd}/${targetFolder}`));
|
||||
}
|
||||
}
|
||||
|
||||
this.$refs.fileUploader.addFiles(fileList, sanitize(`${this.cwd}/${targetFolder}`));
|
||||
} else {
|
||||
if (!files.length) return;
|
||||
|
||||
@@ -346,16 +430,8 @@ export default {
|
||||
async deleteHandler(files) {
|
||||
if (!files) return;
|
||||
|
||||
function start_and_end(str) {
|
||||
if (str.length > 100) {
|
||||
return str.substr(0, 45) + ' ... ' + str.substr(str.length-45, str.length);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
const confirmed = await this.$refs.inputDialog.confirm({
|
||||
message: this.$t('filemanager.removeDialog.reallyDelete'),
|
||||
// message: start_and_end(files.map((f) => f.name).join(', ')),
|
||||
confirmStyle: 'danger',
|
||||
confirmLabel: this.$t('main.dialog.yes'),
|
||||
rejectLabel: this.$t('main.dialog.no'),
|
||||
@@ -458,7 +534,7 @@ export default {
|
||||
try {
|
||||
await this.uploadRequest;
|
||||
} catch (e) {
|
||||
console.log('Upload cancelled.');
|
||||
console.log('Upload cancelled.', e);
|
||||
}
|
||||
|
||||
this.uploadRequest = null;
|
||||
@@ -500,11 +576,11 @@ export default {
|
||||
}
|
||||
|
||||
while(true) {
|
||||
let error, result;
|
||||
let result;
|
||||
try {
|
||||
result = await superagent.get(`${this.apiOrigin}/api/v1/apps/${this.resourceId}`).query({ access_token: this.accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
console.error('Failed to fetch app status.', e);
|
||||
}
|
||||
|
||||
if (result && result.statusCode === 200 && result.body.installationState === ISTATES.INSTALLED) break;
|
||||
@@ -514,76 +590,6 @@ export default {
|
||||
|
||||
this.busyRestart = false;
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.busy = true;
|
||||
const type = this.$route.params.type || 'app';
|
||||
const resourceId = this.$route.params.resourceId;
|
||||
const cwd = this.$route.params.cwd;
|
||||
|
||||
if (type === 'app') {
|
||||
let error, result;
|
||||
try {
|
||||
result = await superagent.get(`${this.apiOrigin}/api/v1/apps/${resourceId}`).query({ access_token: this.accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (error || result.statusCode !== 200) {
|
||||
console.error(`Invalid resource ${type} ${resourceId}`, error || result.statusCode);
|
||||
return this.onFatalError(`Invalid resource ${type} ${resourceId}`);
|
||||
}
|
||||
|
||||
this.appLink = `https://${result.body.fqdn}`;
|
||||
this.title = `${result.body.label || result.body.fqdn} (${result.body.manifest.title})`;
|
||||
} else if (type === 'volume') {
|
||||
let error, result;
|
||||
try {
|
||||
result = await superagent.get(`${this.apiOrigin}/api/v1/volumes/${resourceId}`).query({ access_token: this.accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (error || result.statusCode !== 200) {
|
||||
console.error(`Invalid resource ${type} ${resourceId}`, error || result.statusCode);
|
||||
return this.onFatalError(`Invalid resource ${type} ${resourceId}`);
|
||||
}
|
||||
|
||||
this.title = result.body.name;
|
||||
} else {
|
||||
return this.onFatalError(`Unsupported type ${type}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await superagent.get(`${this.apiOrigin}/api/v1/dashboard/config`).query({ access_token: this.accessToken });
|
||||
this.footerContent = marked.parse(result.body.footer);
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch Cloudron config.', e);
|
||||
}
|
||||
|
||||
window.document.title = `File Manager - ${this.title}`;
|
||||
|
||||
this.cwd = sanitize('/' + (cwd ? cwd.join('/') : '/'));
|
||||
this.resourceType = type;
|
||||
this.resourceId = resourceId;
|
||||
|
||||
this.directoryModel = createDirectoryModel(this.apiOrigin, this.accessToken, type === 'volume' ? `volumes/${resourceId}` : `apps/${resourceId}`);
|
||||
this.ownersModel = this.directoryModel.ownersModel;
|
||||
|
||||
this.loadCwd();
|
||||
|
||||
this.$watch(() => this.$route.params, (toParams, previousParams) => {
|
||||
if (toParams.type !== 'app' && toParams.type !== 'volume') return this.onFatalError(`Unknown type ${toParams.type}`);
|
||||
|
||||
if ((toParams.type !== this.resourceType) || (toParams.resourceId !== this.resourceId)) {
|
||||
this.resourceType = toParams.type;
|
||||
this.resourceId = toParams.resourceId;
|
||||
|
||||
this.directoryModel = createDirectoryModel(this.apiOrigin, this.accessToken, toParams.type === 'volume' ? `volumes/${toParams.resourceId}` : `apps/${toParams.resourceId}`);
|
||||
}
|
||||
|
||||
this.cwd = toParams.cwd ? `/${toParams.cwd.join('/')}` : '/';
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+42
-23
@@ -19,16 +19,18 @@ readonly HELP_MESSAGE="
|
||||
Cloudron Support and Diagnostics Tool
|
||||
|
||||
Options:
|
||||
--disable-dnssec Disable DNSSEC
|
||||
--enable-remote-access Enable SSH Remote Access for the Cloudron support team
|
||||
--patch Apply a patch from git. WARNING: Do not use unless you know what you are doing!
|
||||
--recreate-containers Deletes all existing containers and recreates them without loss of data
|
||||
--recreate-docker Deletes docker storage (containers and images) and recreates it without loss of data
|
||||
--send-diagnostics Collects server diagnostics and uploads it to ${PASTEBIN}
|
||||
--troubleshoot Dashboard down? Run tests to identify the potential problem
|
||||
--owner-login Login as owner
|
||||
--use-external-dns Forwards all DNS requests to Google (8.8.8.8) and Cloudflare (1.1.1.1) DNS servers
|
||||
--help Show this message
|
||||
--disable-dnssec Disable DNSSEC
|
||||
--enable-remote-access Enable SSH Remote Access for the Cloudron support team
|
||||
--patch Apply a patch from git. WARNING: Do not use unless you know what you are doing!
|
||||
--recreate-containers Deletes all existing containers and recreates them without loss of data
|
||||
--recreate-docker Deletes docker storage (containers and images) and recreates it without loss of data
|
||||
--send-diagnostics Collects server diagnostics and uploads it to ${PASTEBIN}
|
||||
--troubleshoot Dashboard down? Run tests to identify the potential problem
|
||||
--owner-login Login as owner
|
||||
--unbound-use-external-dns Forwards all Unbound requests to Google (8.8.8.8) and Cloudflare (1.1.1.1) DNS servers.
|
||||
Unbound is the internal DNS server used for recursive DNS queries. This is only needed
|
||||
if your network does not allow outbound DNS requests.
|
||||
--help Show this message
|
||||
"
|
||||
|
||||
function success() {
|
||||
@@ -133,11 +135,10 @@ function check_netplan() {
|
||||
fi
|
||||
|
||||
if [[ -z "${output}" ]]; then
|
||||
fail "netplan configuration is empty"
|
||||
exit 1
|
||||
warn "netplan configuration is empty. this might be OK depending on your networking setup"
|
||||
else
|
||||
success "netplan is good"
|
||||
fi
|
||||
|
||||
success "netplan is good"
|
||||
}
|
||||
|
||||
function owner_login() {
|
||||
@@ -217,13 +218,31 @@ function send_diagnostics() {
|
||||
}
|
||||
|
||||
function check_dns() {
|
||||
if ! host cloudron.io &>/dev/null; then
|
||||
fail "DNS is not resolving"
|
||||
host cloudron.io
|
||||
exit 1
|
||||
if host cloudron.io &>/dev/null; then
|
||||
success "DNS is resolving via systemd-resolved"
|
||||
return
|
||||
fi
|
||||
|
||||
success "DNS is resolving via systemd-resolved"
|
||||
if ! systemctl is-active -q systemd-resolved; then
|
||||
warn "systemd-resolved is not in use. see 'systemctl status systemd-resolved'"
|
||||
fi
|
||||
|
||||
if [[ -L /etc/resolv.conf ]]; then
|
||||
target=$(readlink /etc/resolv.conf)
|
||||
if [[ "$target" != *"/run/systemd/resolve/stub-resolv.conf" ]]; then
|
||||
warn "/etc/resolv.conf is symlinked to $target instead of '../run/systemd/resolve/stub-resolv.conf'"
|
||||
fi
|
||||
else
|
||||
warn "/etc/resolv.conf is not symlinked to '../run/systemd/resolve/stub-resolv.conf'"
|
||||
fi
|
||||
|
||||
if ! grep -q "^nameserver 127.0.0.53" /etc/resolv.conf; then
|
||||
warn "/etc/resolv.conf is not using systemd-resolved. it is missing the line 'nameserver 127.0.0.53'"
|
||||
fi
|
||||
|
||||
fail "DNS is not resolving"
|
||||
host cloudron.io || true
|
||||
exit 1
|
||||
}
|
||||
|
||||
function check_unbound() {
|
||||
@@ -244,7 +263,7 @@ function check_unbound() {
|
||||
fi
|
||||
|
||||
if ! host cloudron.io 127.0.0.150 &>/dev/null; then
|
||||
fail "Unbound is not resolving, maybe try forwarding all DNS requests. You can do this by running 'cloudron-support --use-external-dns' option"
|
||||
fail "Unbound is not resolving, maybe try forwarding all DNS requests. You can do this by running 'cloudron-support --unbound-use-external-dns' option"
|
||||
host cloudron.io 127.0.0.150
|
||||
exit 1
|
||||
fi
|
||||
@@ -430,7 +449,7 @@ function check_expired_domain() {
|
||||
success "Domain ${dashboard_domain} is valid and has not expired"
|
||||
}
|
||||
|
||||
function use_external_dns() {
|
||||
function unbount_use_external_dns() {
|
||||
local -r conf_file="/etc/unbound/unbound.conf.d/forward-everything.conf"
|
||||
|
||||
info "To remove the forwarding, please delete $conf_file and 'systemctl restart unbound'"
|
||||
@@ -658,7 +677,7 @@ function apply_patch() {
|
||||
|
||||
check_disk_space
|
||||
|
||||
args=$(getopt -o "" -l "admin-login,disable-dnssec,enable-ssh,enable-remote-access,help,owner-login,patch:,recreate-containers,recreate-docker,send-diagnostics,use-external-dns,troubleshoot" -n "$0" -- "$@")
|
||||
args=$(getopt -o "" -l "admin-login,disable-dnssec,enable-ssh,enable-remote-access,help,owner-login,patch:,recreate-containers,recreate-docker,send-diagnostics,unbound-use-external-dns,troubleshoot" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
@@ -674,7 +693,7 @@ while true; do
|
||||
--send-diagnostics) send_diagnostics; exit 0;;
|
||||
--troubleshoot) troubleshoot; exit 0;;
|
||||
--disable-dnssec) disable_dnssec; exit 0;;
|
||||
--use-external-dns) use_external_dns; exit 0;;
|
||||
--unbound-use-external-dns) unbound_use_external_dns; exit 0;;
|
||||
--recreate-containers) recreate_containers; exit 0;;
|
||||
--recreate-docker) recreate_docker; exit 0;;
|
||||
--patch) apply_patch "$2"; exit 0;;
|
||||
|
||||
+2
-1
@@ -2628,8 +2628,9 @@ async function autoupdateApps(updateInfo, auditSource) { // updateInfo is { appI
|
||||
force: false
|
||||
};
|
||||
|
||||
debug(`app ${app.fqdn} will be automatically updated`);
|
||||
const [updateError] = await safe(updateApp(app, data, auditSource));
|
||||
if (updateError) debug(`Error initiating autoupdate of ${appId}. ${updateError.message}`);
|
||||
if (updateError) debug(`Error autoupdating ${appId}. ${updateError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ function applyBackupRetention(allBackups, retention, referencedBackupIds) {
|
||||
}
|
||||
|
||||
if (retention.keepLatest) {
|
||||
let latestNormalBackup = allBackups.find(b => b.state === backups.BACKUP_STATE_NORMAL);
|
||||
const latestNormalBackup = allBackups.find(b => b.state === backups.BACKUP_STATE_NORMAL);
|
||||
if (latestNormalBackup && !latestNormalBackup.keepReason) latestNormalBackup.keepReason = 'latest';
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ async function cleanupAppBackups(backupConfig, retention, referencedBackupIds, p
|
||||
const appBackups = await backups.getByTypePaged(backups.BACKUP_TYPE_APP, 1, 1000);
|
||||
|
||||
// collate the backups by app id. note that the app could already have been uninstalled
|
||||
let appBackupsById = {};
|
||||
const appBackupsById = {};
|
||||
for (const appBackup of appBackups) {
|
||||
if (!appBackupsById[appBackup.identifier]) appBackupsById[appBackup.identifier] = [];
|
||||
appBackupsById[appBackup.identifier].push(appBackup);
|
||||
@@ -177,7 +177,8 @@ async function cleanupBoxBackups(backupConfig, retention, progressCallback) {
|
||||
assert.strictEqual(typeof retention, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
|
||||
let referencedBackupIds = [], removedBoxBackupPaths = [];
|
||||
let referencedBackupIds = [];
|
||||
const removedBoxBackupPaths = [];
|
||||
|
||||
const boxBackups = await backups.getByTypePaged(backups.BACKUP_TYPE_BOX, 1, 1000);
|
||||
|
||||
@@ -200,7 +201,7 @@ async function cleanupBoxBackups(backupConfig, retention, progressCallback) {
|
||||
return { removedBoxBackupPaths, referencedBackupIds };
|
||||
}
|
||||
|
||||
// cleans up the database by checking if backup exists in the remote
|
||||
// cleans up the database by checking if backup exists in the remote. this can happen if user had set some bucket policy
|
||||
async function cleanupMissingBackups(backupConfig, progressCallback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
@@ -215,6 +216,8 @@ async function cleanupMissingBackups(backupConfig, progressCallback) {
|
||||
result = await backups.list(page, perPage);
|
||||
|
||||
for (const backup of result) {
|
||||
if (backup.state !== backups.BACKUP_STATE_NORMAL) continue; // note: errored and incomplete backups are cleaned up by the backup retention logic
|
||||
|
||||
let backupFilePath = backupFormat.api(backup.format).getBackupFilePath(backupConfig, backup.remotePath);
|
||||
if (backup.format === 'rsync') backupFilePath = backupFilePath + '/'; // add trailing slash to indicate directory
|
||||
|
||||
|
||||
+2
-2
@@ -223,7 +223,7 @@ async function handleAutoupdatePatternChanged(pattern) {
|
||||
const updateInfo = updateChecker.getUpdateInfo();
|
||||
// do box before app updates. for the off chance that the box logic fixes some app update logic issue
|
||||
if (updateInfo.box && !updateInfo.box.unstable) {
|
||||
debug('Starting box autoupdate to %j', updateInfo.box);
|
||||
debug('Starting box autoupdate to %j', updateInfo.box.version);
|
||||
const [error] = await safe(updater.updateToLatest({ skipBackup: false }, AuditSource.CRON));
|
||||
if (!error) return; // do not start app updates when a box update got scheduled
|
||||
debug(`Failed to start box autoupdate task: ${error.message}`);
|
||||
@@ -232,7 +232,7 @@ async function handleAutoupdatePatternChanged(pattern) {
|
||||
|
||||
const appUpdateInfo = _.omit(updateInfo, 'box');
|
||||
if (Object.keys(appUpdateInfo).length > 0) {
|
||||
debug('Starting app update to %j', appUpdateInfo);
|
||||
debug('Starting app autoupdate: %j', Object.keys(appUpdateInfo));
|
||||
const [error] = await safe(apps.autoupdateApps(appUpdateInfo, AuditSource.CRON));
|
||||
if (error) debug(`Failed to app autoupdate: ${error.message}`);
|
||||
} else {
|
||||
|
||||
@@ -18,7 +18,7 @@ exports = module.exports = {
|
||||
'mysql': 'registry.docker.com/cloudron/mysql:3.4.3@sha256:8934c5ddcd69f24740d9a38f0de2937e47240238f3b8f5c482862eeccc5a21d2',
|
||||
'postgresql': 'registry.docker.com/cloudron/postgresql:5.2.3@sha256:9b7d5147e9c8008e4766cc80ebf4b833f3dfcf19ef0d81b013dfab76995d8d16',
|
||||
'redis': 'registry.docker.com/cloudron/redis:3.5.3@sha256:1e1200900c6fb196950531ecec43f400b4fe5e559fac1c75f21e6f0c11885b5f',
|
||||
'sftp': 'registry.docker.com/cloudron/sftp:3.8.7@sha256:9d13007f665d72875e7e4830fc4d5ee352c7c1a44b4f0f526746e2c2d2c296e0',
|
||||
'sftp': 'registry.docker.com/cloudron/sftp:3.8.9@sha256:f2a126839df99ca420a3ad8177594f58b113f6292e98719f2cf2e0ddc3597696',
|
||||
'turn': 'registry.docker.com/cloudron/turn:1.7.2@sha256:9ed8da613c1edc5cb8700657cf6e49f0f285b446222a8f459f80919945352f6d',
|
||||
}
|
||||
};
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ function tail(filePaths, options) {
|
||||
if (options.follow) args.push('--follow');
|
||||
|
||||
if (options.sudo) {
|
||||
return shell.sudo('tail', args.concat(filePaths), { streamStdout: true }, () => {});
|
||||
return shell.sudo('tail', args.concat(filePaths), { quiet: true }, () => {});
|
||||
} else {
|
||||
const cp = spawn('/usr/bin/tail', args.concat(filePaths));
|
||||
cp.terminate = () => cp.kill('SIGKILL');
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
exports = module.exports = {
|
||||
cookieParser: require('cookie-parser'),
|
||||
cors: require('./cors.js'),
|
||||
proxy: require('./proxy-middleware.js'),
|
||||
lastMile: require('connect-lastmile'),
|
||||
multipart: require('./multipart.js'),
|
||||
timeout: require('connect-timeout')
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
// https://github.com/cloudron-io/node-proxy-middleware
|
||||
// MIT license
|
||||
// contains https://github.com/gonzalocasas/node-proxy-middleware/pull/59
|
||||
|
||||
var os = require('os');
|
||||
var http = require('http');
|
||||
var https = require('https');
|
||||
var owns = {}.hasOwnProperty;
|
||||
|
||||
module.exports = function proxyMiddleware(options) {
|
||||
//enable ability to quickly pass a url for shorthand setup
|
||||
if(typeof options === 'string'){
|
||||
options = require('url').parse(options);
|
||||
}
|
||||
|
||||
var httpLib = options.protocol === 'https:' ? https : http;
|
||||
var request = httpLib.request;
|
||||
|
||||
options = options || {};
|
||||
options.hostname = options.hostname;
|
||||
options.port = options.port;
|
||||
options.pathname = options.pathname || '/';
|
||||
|
||||
return function (req, resp, next) {
|
||||
var url = req.url;
|
||||
// You can pass the route within the options, as well
|
||||
if (typeof options.route === 'string') {
|
||||
if (url === options.route) {
|
||||
url = '';
|
||||
} else if (url.slice(0, options.route.length) === options.route) {
|
||||
url = url.slice(options.route.length);
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
//options for this request
|
||||
var opts = extend({}, options);
|
||||
if (url && url.charAt(0) === '?') { // prevent /api/resource/?offset=0
|
||||
if (options.pathname.length > 1 && options.pathname.charAt(options.pathname.length - 1) === '/') {
|
||||
opts.path = options.pathname.substring(0, options.pathname.length - 1) + url;
|
||||
} else {
|
||||
opts.path = options.pathname + url;
|
||||
}
|
||||
} else if (url) {
|
||||
opts.path = slashJoin(options.pathname, url);
|
||||
} else {
|
||||
opts.path = options.pathname;
|
||||
}
|
||||
opts.method = req.method;
|
||||
opts.headers = options.headers ? merge(req.headers, options.headers) : req.headers;
|
||||
|
||||
applyViaHeader(req.headers, opts, opts.headers);
|
||||
|
||||
if (!options.preserveHost) {
|
||||
// Forwarding the host breaks dotcloud
|
||||
delete opts.headers.host;
|
||||
}
|
||||
|
||||
var myReq = request(opts, function (myRes) {
|
||||
var statusCode = myRes.statusCode
|
||||
, headers = myRes.headers
|
||||
, location = headers.location;
|
||||
// Fix the location
|
||||
if (((statusCode > 300 && statusCode < 304) || statusCode === 201) && location && location.indexOf(options.href) > -1) {
|
||||
// absoulte path
|
||||
headers.location = location.replace(options.href, slashJoin('/', slashJoin((options.route || ''), '')));
|
||||
}
|
||||
applyViaHeader(myRes.headers, opts, myRes.headers);
|
||||
rewriteCookieHosts(myRes.headers, opts, myRes.headers, req);
|
||||
resp.writeHead(myRes.statusCode, myRes.headers);
|
||||
myRes.on('error', function (err) {
|
||||
next(err);
|
||||
});
|
||||
myRes.on('end', function (err) {
|
||||
next();
|
||||
});
|
||||
myRes.pipe(resp);
|
||||
});
|
||||
myReq.on('error', function (err) {
|
||||
next(err);
|
||||
});
|
||||
if (!req.readable) {
|
||||
myReq.end();
|
||||
} else {
|
||||
req.pipe(myReq);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
function applyViaHeader(existingHeaders, opts, applyTo) {
|
||||
if (!opts.via) return;
|
||||
|
||||
var viaName = (true === opts.via) ? os.hostname() : opts.via;
|
||||
var viaHeader = '1.1 ' + viaName;
|
||||
if(existingHeaders.via) {
|
||||
viaHeader = existingHeaders.via + ', ' + viaHeader;
|
||||
}
|
||||
|
||||
applyTo.via = viaHeader;
|
||||
}
|
||||
|
||||
function rewriteCookieHosts(existingHeaders, opts, applyTo, req) {
|
||||
if (!opts.cookieRewrite || !owns.call(existingHeaders, 'set-cookie')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var existingCookies = existingHeaders['set-cookie'],
|
||||
rewrittenCookies = [],
|
||||
rewriteHostname = (true === opts.cookieRewrite) ? os.hostname() : opts.cookieRewrite;
|
||||
|
||||
if (!Array.isArray(existingCookies)) {
|
||||
existingCookies = [ existingCookies ];
|
||||
}
|
||||
|
||||
for (var i = 0; i < existingCookies.length; i++) {
|
||||
var rewrittenCookie = existingCookies[i].replace(/(Domain)=[a-z\.-_]*?(;|$)/gi, '$1=' + rewriteHostname + '$2');
|
||||
|
||||
if (!req.connection.encrypted) {
|
||||
rewrittenCookie = rewrittenCookie.replace(/;\s*?(Secure)/i, '');
|
||||
}
|
||||
rewrittenCookies.push(rewrittenCookie);
|
||||
}
|
||||
|
||||
applyTo['set-cookie'] = rewrittenCookies;
|
||||
}
|
||||
|
||||
function slashJoin(p1, p2) {
|
||||
var trailing_slash = false;
|
||||
|
||||
if (p1.length && p1[p1.length - 1] === '/') { trailing_slash = true; }
|
||||
if (trailing_slash && p2.length && p2[0] === '/') {p2 = p2.substring(1); }
|
||||
|
||||
return p1 + p2;
|
||||
}
|
||||
|
||||
function extend(obj, src) {
|
||||
for (var key in src) if (owns.call(src, key)) obj[key] = src[key];
|
||||
return obj;
|
||||
}
|
||||
|
||||
//merges data without changing state in either argument
|
||||
function merge(src1, src2) {
|
||||
var merged = {};
|
||||
extend(merged, src1);
|
||||
extend(merged, src2);
|
||||
return merged;
|
||||
}
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ function validateMountOptions(type, options) {
|
||||
}
|
||||
}
|
||||
|
||||
// managed providers are those for which we setup systemd mount file
|
||||
// managed providers are those for which we setup systemd mount file under /mnt/volumes
|
||||
function isManagedProvider(provider) {
|
||||
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'ext4' || provider === 'xfs' || provider === 'disk';
|
||||
}
|
||||
|
||||
+27
-20
@@ -6,11 +6,10 @@ exports = module.exports = {
|
||||
|
||||
const assert = require('assert'),
|
||||
BoxError = require('../boxerror.js'),
|
||||
middleware = require('../middleware/index.js'),
|
||||
http = require('http'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
safe = require('safetydance'),
|
||||
services = require('../services.js'),
|
||||
url = require('url');
|
||||
services = require('../services.js');
|
||||
|
||||
function proxy(kind) {
|
||||
assert(kind === 'mail' || kind === 'volume' || kind === 'app');
|
||||
@@ -25,25 +24,33 @@ function proxy(kind) {
|
||||
case 'mail': id = 'mail'; break;
|
||||
}
|
||||
|
||||
const [error, result] = await safe(services.getContainerDetails('sftp', 'CLOUDRON_SFTP_TOKEN'));
|
||||
const [error, addonDetails] = await safe(services.getContainerDetails('sftp', 'CLOUDRON_SFTP_TOKEN'));
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
const parsedUrl = url.parse(req.url, true /* parseQueryString */);
|
||||
parsedUrl.query['access_token'] = result.token;
|
||||
const searchParams = new URLSearchParams(req.url.slice(req.url.indexOf('?')+1));
|
||||
searchParams.delete('access_token');
|
||||
searchParams.append('access_token', addonDetails.token);
|
||||
|
||||
req.url = url.format({ pathname: `/files/${id}/${encodeURIComponent(req.params[0])}`, query: parsedUrl.query }); // params[0] already contains leading '/'
|
||||
const opts = {
|
||||
hostname: addonDetails.ip,
|
||||
port: 3000,
|
||||
path: `/files/${id}/${encodeURIComponent(req.params[0])}?${searchParams.toString()}`, // params[0] already contains leading '/'
|
||||
method: req.method,
|
||||
headers: req.headers
|
||||
};
|
||||
|
||||
const proxyOptions = url.parse(`http://${result.ip}:3000`);
|
||||
proxyOptions.rejectUnauthorized = false;
|
||||
const fileManagerProxy = middleware.proxy(proxyOptions);
|
||||
|
||||
fileManagerProxy(req, res, function (error) {
|
||||
if (!error) return next();
|
||||
|
||||
if (error.code === 'ECONNREFUSED') return next(new HttpError(424, 'Unable to connect to filemanager server'));
|
||||
if (error.code === 'ECONNRESET') return next(new HttpError(424, 'Unable to query filemanager server'));
|
||||
|
||||
next(new HttpError(500, error));
|
||||
});
|
||||
};
|
||||
const sftpReq = http.request(opts, function (sftpRes) {
|
||||
res.writeHead(sftpRes.statusCode, sftpRes.headers);
|
||||
// note: these are intentionally not handled. response has already been written. do not forward to connect-lastmile
|
||||
// sftpRes.on('error', (error) => next(new HttpError(500, `filemanager error: ${error.message} ${error.code}`)));
|
||||
// sftpRes.on('end', () => next());
|
||||
sftpRes.pipe(res);
|
||||
});
|
||||
sftpReq.on('error', (error) => next(new HttpError(424, `Unable to connect to filemanager: ${error.message} ${error.code}`)));
|
||||
if (!req.readable) {
|
||||
sftpReq.end();
|
||||
} else {
|
||||
req.pipe(sftpReq);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
+24
-21
@@ -13,40 +13,43 @@ const assert = require('assert'),
|
||||
AuditSource = require('../auditsource.js'),
|
||||
BoxError = require('../boxerror.js'),
|
||||
debug = require('debug')('box:routes/mailserver'),
|
||||
http = require('http'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
mailServer = require('../mailserver.js'),
|
||||
middleware = require('../middleware/index.js'),
|
||||
safe = require('safetydance'),
|
||||
services = require('../services.js'),
|
||||
url = require('url');
|
||||
services = require('../services.js');
|
||||
|
||||
async function proxyToMailContainer(port, pathname, req, res, next) {
|
||||
const parsedUrl = url.parse(req.url, true /* parseQueryString */);
|
||||
|
||||
// do not proxy protected values
|
||||
delete parsedUrl.query['access_token'];
|
||||
delete req.headers['authorization'];
|
||||
delete req.headers['cookies'];
|
||||
req.clearTimeout();
|
||||
|
||||
const [error, addonDetails] = await safe(services.getContainerDetails('mail', 'CLOUDRON_MAIL_TOKEN'));
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
parsedUrl.query['access_token'] = addonDetails.token;
|
||||
req.url = url.format({ pathname, query: parsedUrl.query });
|
||||
const searchParams = new URLSearchParams(req.url.slice(req.url.indexOf('?')+1));
|
||||
searchParams.delete('access_token');
|
||||
searchParams.append('access_token', addonDetails.token);
|
||||
|
||||
const proxyOptions = url.parse(`http://${addonDetails.ip}:${port}`);
|
||||
const mailserverProxy = middleware.proxy(proxyOptions);
|
||||
const opts = {
|
||||
hostname: addonDetails.ip,
|
||||
port: 3000,
|
||||
path: `/${pathname}?${searchParams.toString()}`,
|
||||
method: req.method,
|
||||
headers: req.headers
|
||||
};
|
||||
|
||||
req.clearTimeout(); // TODO: add timeout to mail server proxy logic instead of this
|
||||
mailserverProxy(req, res, function (error) {
|
||||
if (!error) return next(); // note: response was already sent by proxy by this point
|
||||
|
||||
if (error.code === 'ECONNREFUSED') return next(new HttpError(424, 'Unable to connect to mail server'));
|
||||
if (error.code === 'ECONNRESET') return next(new HttpError(424, 'Unable to query mail server'));
|
||||
|
||||
next(new HttpError(500, error));
|
||||
const sftpReq = http.request(opts, function (sftpRes) {
|
||||
res.writeHead(sftpRes.statusCode, sftpRes.headers);
|
||||
sftpRes.on('error', (error) => next(new HttpError(500, `mailserver error: ${error.message} ${error.code}`)));
|
||||
sftpRes.on('end', () => next());
|
||||
sftpRes.pipe(res);
|
||||
});
|
||||
sftpReq.on('error', (error) => next(new HttpError(424, `Unable to connect to mailserver: ${error.message} ${error.code}`)));
|
||||
if (!req.readable) {
|
||||
sftpReq.end();
|
||||
} else {
|
||||
req.pipe(sftpReq);
|
||||
}
|
||||
}
|
||||
|
||||
async function proxy(req, res, next) {
|
||||
|
||||
+10
-1
@@ -50,7 +50,16 @@ async function providerTokenAuth(req, res, next) {
|
||||
if (system.getProvider() === 'ami') {
|
||||
if (typeof req.body.providerToken !== 'string' || !req.body.providerToken) return next(new HttpError(400, 'providerToken must be a non empty string'));
|
||||
|
||||
const [error, response] = await safe(superagent.get('http://169.254.169.254/latest/meta-data/instance-id').timeout(30 * 1000).ok(() => true));
|
||||
|
||||
// IMDSv2 https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-options.html
|
||||
// https://aws.amazon.com/blogs/security/defense-in-depth-open-firewalls-reverse-proxies-ssrf-vulnerabilities-ec2-instance-metadata-service/
|
||||
const imdsIp = req.body.ipv4Config?.provider === 'noop' ? '[fd00:ec2::254]' : '169.254.169.254'; // use ipv4config carefully, it's not validated yet at this point
|
||||
const [tokenError, tokenResponse] = await safe(superagent.put(`http://${imdsIp}/latest/api/token`).set('x-aws-ec2-metadata-token-ttl-seconds', 600).timeout(30 * 1000).ok(() => true));
|
||||
if (tokenError) return next(new HttpError(422, `Network error getting EC2 metadata session token: ${tokenError.message}`));
|
||||
if (tokenResponse.status !== 200) return next(new HttpError(422, `Unable to get EC2 meta data session token. statusCode: ${tokenResponse.status}`));
|
||||
const imdsToken = tokenResponse.text;
|
||||
|
||||
const [error, response] = await safe(superagent.get(`http://${imdsIp}/latest/meta-data/instance-id`).set('x-aws-ec2-metadata-token', imdsToken).timeout(30 * 1000).ok(() => true));
|
||||
if (error) return next(new HttpError(422, `Network error getting EC2 metadata: ${error.message}`));
|
||||
if (response.status !== 200) return next(new HttpError(422, `Unable to get EC2 meta data. statusCode: ${response.status}`));
|
||||
if (response.text !== req.body.providerToken) return next(new HttpError(422, 'Instance ID does not match'));
|
||||
|
||||
@@ -29,6 +29,6 @@ while true; do
|
||||
done
|
||||
|
||||
# first sort the existing log lines
|
||||
tail --quiet --lines=${lines} -- "$@" | sort -k1 || true # ignore error if files are missing
|
||||
tail --quiet --lines=${lines} "$@" | sort -k1 || true # ignore error if files are missing
|
||||
|
||||
exec tail ${follow} --lines=0 "$@"
|
||||
|
||||
+1
-1
@@ -1792,7 +1792,7 @@ async function startRedis(existingInfra) {
|
||||
const allApps = await apps.list();
|
||||
|
||||
for (const app of allApps) {
|
||||
if (!('redis' in app.manifest.addons)) continue; // app doesn't use the addon
|
||||
if (!app.manifest.addons || !('redis' in app.manifest.addons)) continue; // app doesn't use the addon
|
||||
|
||||
const redisName = `redis-${app.id}`;
|
||||
|
||||
|
||||
+4
-3
@@ -15,6 +15,7 @@ const apps = require('./apps.js'),
|
||||
docker = require('./docker.js'),
|
||||
hat = require('./hat.js'),
|
||||
infra = require('./infra_version.js'),
|
||||
mounts = require('./mounts.js'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
@@ -82,7 +83,7 @@ async function start(existingInfra) {
|
||||
dataDirs.push({ hostDir: '/mnt/volumes', mountDir: '/mnt/volumes' }); // managed volumes
|
||||
const allVolumes = await volumes.list();
|
||||
for (const volume of allVolumes) {
|
||||
if (volume.hostPath.startsWith('/mnt/volumes/')) continue; // skip managed volume
|
||||
if (mounts.isManagedProvider(volume.mountType)) continue; // skip managed volume. these are acessed via /mnt/volumes mount above
|
||||
|
||||
if (!safe.fs.existsSync(volume.hostPath)) {
|
||||
debug(`Ignoring volume host path ${volume.hostPath} since it does not exist`);
|
||||
@@ -100,7 +101,7 @@ async function start(existingInfra) {
|
||||
const readOnly = !serviceConfig.recoveryMode ? '--read-only' : '';
|
||||
const cmd = serviceConfig.recoveryMode ? '/bin/bash -c \'echo "Debug mode. Sleeping" && sleep infinity\'' : '';
|
||||
|
||||
const mounts = dataDirs.map(v => `-v "${v.hostDir}:${v.mountDir}"`).join(' ');
|
||||
const volumeMounts = dataDirs.map(v => `-v "${v.hostDir}:${v.mountDir}"`).join(' ');
|
||||
const runCmd = `docker run --restart=always -d --name=sftp \
|
||||
--hostname sftp \
|
||||
--net cloudron \
|
||||
@@ -112,7 +113,7 @@ async function start(existingInfra) {
|
||||
-m ${memoryLimit} \
|
||||
--memory-swap -1 \
|
||||
-p 222:22 \
|
||||
${mounts} \
|
||||
${volumeMounts} \
|
||||
-e CLOUDRON_SFTP_TOKEN=${cloudronToken} \
|
||||
-v ${paths.SFTP_KEYS_DIR}:/etc/ssh:ro \
|
||||
--label isCloudronManaged=true \
|
||||
|
||||
+1
-1
@@ -111,7 +111,7 @@ function sudo(tag, args, options, callback) {
|
||||
|
||||
cp.stdout.on('data', (data) => {
|
||||
if (options.captureStdout) stdoutResult += data.toString('utf8');
|
||||
process.stdout.write(data); // do not use debug to avoid double timestamps when calling backupupload.js
|
||||
if (!options.quiet) process.stdout.write(data); // do not use debug to avoid double timestamps when calling backupupload.js
|
||||
});
|
||||
cp.stderr.on('data', (data) => {
|
||||
process.stderr.write(data); // do not use debug to avoid double timestamps when calling backupupload.js
|
||||
|
||||
@@ -171,7 +171,7 @@ async function copy(apiConfig, oldFilePath, newFilePath, progressCallback) {
|
||||
cpOptions += apiConfig.noHardlinks ? '' : 'l'; // this will hardlink backups saving space
|
||||
|
||||
if (apiConfig.provider === PROVIDER_SSHFS) {
|
||||
const identityFilePath = `/home/yellowtent/platformdata/sshfs/id_rsa_${apiConfig.mountOptions.host}`;
|
||||
const identityFilePath = path.join(paths.SSHFS_KEYS_DIR, `id_rsa_${apiConfig.mountOptions.host}`);
|
||||
|
||||
const sshOptions = [ '-o', '"StrictHostKeyChecking no"', '-i', identityFilePath, '-p', apiConfig.mountOptions.port, `${apiConfig.mountOptions.user}@${apiConfig.mountOptions.host}` ];
|
||||
const sshArgs = sshOptions.concat([ 'cp', cpOptions, oldFilePath.replace('/mnt/cloudronbackup/', ''), newFilePath.replace('/mnt/cloudronbackup/', '') ]);
|
||||
|
||||
+10
-6
@@ -21,7 +21,8 @@ exports = module.exports = {
|
||||
|
||||
const assert = require('assert'),
|
||||
BoxError = require('../boxerror.js'),
|
||||
debug = require('debug')('box:storage/noop');
|
||||
debug = require('debug')('box:storage/noop'),
|
||||
fs = require('fs');
|
||||
|
||||
async function getProviderStatus(apiConfig) {
|
||||
assert.strictEqual(typeof apiConfig, 'object');
|
||||
@@ -35,15 +36,18 @@ async function getAvailableSize(apiConfig) {
|
||||
return Number.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
function upload(apiConfig, backupFilePath, sourceStream, callback) {
|
||||
async function upload(apiConfig, backupFilePath) {
|
||||
assert.strictEqual(typeof apiConfig, 'object');
|
||||
assert.strictEqual(typeof backupFilePath, 'string');
|
||||
assert.strictEqual(typeof sourceStream, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('upload: %s', backupFilePath);
|
||||
debug(`upload: ${backupFilePath}`);
|
||||
|
||||
callback(null);
|
||||
const uploadStream = fs.createWriteStream('/dev/null');
|
||||
|
||||
return {
|
||||
stream: uploadStream,
|
||||
async finish() {}
|
||||
};
|
||||
}
|
||||
|
||||
async function exists(apiConfig, backupFilePath) {
|
||||
|
||||
+1
-1
@@ -133,7 +133,7 @@ async function upload(apiConfig, backupFilePath) {
|
||||
stream: passThrough,
|
||||
async finish() {
|
||||
const [error, data] = await safe(uploadPromise);
|
||||
if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, `Upload error: ${error.message}`);
|
||||
if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, `Upload error: code: ${error.code} message: ${error.message}`); // sometimes message is null
|
||||
debug(`Upload finished. ${JSON.stringify(data)}`);
|
||||
}
|
||||
};
|
||||
|
||||
+4
-1
@@ -55,10 +55,13 @@ function validateHostPath(hostPath, mountType) {
|
||||
if (hostPath === '/') return new BoxError(BoxError.BAD_FIELD, 'hostPath cannot be /');
|
||||
|
||||
if (!hostPath.endsWith('/')) hostPath = hostPath + '/'; // ensure trailing slash for the prefix matching to work
|
||||
const allowedPaths = [ '/mnt/', '/media/', '/srv/', '/opt/' ];
|
||||
|
||||
const allowedPaths = [ '/mnt/', '/media/', '/srv/', '/opt/' ];
|
||||
if (!allowedPaths.some(p => hostPath.startsWith(p))) return new BoxError(BoxError.BAD_FIELD, 'hostPath must be under /mnt, /media, /opt or /srv');
|
||||
|
||||
const reservedPaths = [ `${paths.VOLUMES_MOUNT_DIR}/` ];
|
||||
if (reservedPaths.some(p => hostPath.startsWith(p))) return new BoxError(BoxError.BAD_FIELD, 'hostPath is reserved');
|
||||
|
||||
if (!constants.TEST) { // we expect user to have already mounted this
|
||||
const stat = safe.fs.lstatSync(hostPath);
|
||||
if (!stat) return new BoxError(BoxError.BAD_FIELD, 'hostPath does not exist. Please create it on the server first');
|
||||
|
||||
Reference in New Issue
Block a user