Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
3e4a67f350 | |||
e0e5b4426a | |||
7158b304f5 | |||
833eca6d2f | |||
151acf5d96 | |||
4a363fb4a7 | |||
229adeb746 | |||
1eff9a1e89 | |||
ed4f0a0edc | |||
13bed6226e | |||
d937daedb3 | |||
8cce51135c | |||
0cfe1b3f13 | |||
05c16a6e59 | |||
3294b516d3 | |||
139bcedc53 | |||
cf9ea3c4c7 | |||
e84fde3e14 | |||
1de47507ff | |||
1a9948a488 |
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "proxmox-backup"
|
name = "proxmox-backup"
|
||||||
version = "0.8.15"
|
version = "0.8.16"
|
||||||
authors = ["Dietmar Maurer <dietmar@proxmox.com>"]
|
authors = ["Dietmar Maurer <dietmar@proxmox.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3"
|
license = "AGPL-3"
|
||||||
@ -26,7 +26,7 @@ futures = "0.3"
|
|||||||
h2 = { version = "0.2", features = ["stream"] }
|
h2 = { version = "0.2", features = ["stream"] }
|
||||||
handlebars = "3.0"
|
handlebars = "3.0"
|
||||||
http = "0.2"
|
http = "0.2"
|
||||||
hyper = "0.13"
|
hyper = "0.13.6"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
22
debian/changelog
vendored
22
debian/changelog
vendored
@ -1,3 +1,25 @@
|
|||||||
|
rust-proxmox-backup (0.8.16-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* BackupDir: make constructor fallible
|
||||||
|
|
||||||
|
* handle invalid mtime when formating entries
|
||||||
|
|
||||||
|
* ui/docs: add onlineHelp button for syncjobs
|
||||||
|
|
||||||
|
* docs: add section for calendar events
|
||||||
|
|
||||||
|
* tools/systemd/parse_time: enable */x syntax for calendar events
|
||||||
|
|
||||||
|
* docs: set html img width limitation through css
|
||||||
|
|
||||||
|
* docs: use alabaster theme
|
||||||
|
|
||||||
|
* server: set http2 max frame size
|
||||||
|
|
||||||
|
* doc: Add section "FAQ"
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Fri, 11 Sep 2020 15:54:57 +0200
|
||||||
|
|
||||||
rust-proxmox-backup (0.8.15-1) unstable; urgency=medium
|
rust-proxmox-backup (0.8.15-1) unstable; urgency=medium
|
||||||
|
|
||||||
* verify: skip benchmark directory
|
* verify: skip benchmark directory
|
||||||
|
@ -76,6 +76,8 @@ onlinehelpinfo:
|
|||||||
.PHONY: html
|
.PHONY: html
|
||||||
html: ${GENERATED_SYNOPSIS}
|
html: ${GENERATED_SYNOPSIS}
|
||||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||||
|
cp images/proxmox-logo.svg $(BUILDDIR)/html/_static/
|
||||||
|
cp custom.css $(BUILDDIR)/html/_static/
|
||||||
@echo
|
@echo
|
||||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||||
|
|
||||||
|
@ -150,7 +150,6 @@ Disk Management
|
|||||||
~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
.. image:: images/screenshots/pbs-gui-disks.png
|
.. image:: images/screenshots/pbs-gui-disks.png
|
||||||
:width: 230
|
|
||||||
:align: right
|
:align: right
|
||||||
:alt: List of disks
|
:alt: List of disks
|
||||||
|
|
||||||
@ -182,7 +181,6 @@ To initialize a disk with a new GPT, use the ``initialize`` subcommand:
|
|||||||
# proxmox-backup-manager disk initialize sdX
|
# proxmox-backup-manager disk initialize sdX
|
||||||
|
|
||||||
.. image:: images/screenshots/pbs-gui-disks-dir-create.png
|
.. image:: images/screenshots/pbs-gui-disks-dir-create.png
|
||||||
:width: 230
|
|
||||||
:align: right
|
:align: right
|
||||||
:alt: Create a directory
|
:alt: Create a directory
|
||||||
|
|
||||||
@ -193,19 +191,11 @@ web interface and creating one from there. The following command creates an
|
|||||||
automatically create a datastore on the disk (in this case ``sdd``). This will
|
automatically create a datastore on the disk (in this case ``sdd``). This will
|
||||||
create a datastore at the location ``/mnt/datastore/store1``:
|
create a datastore at the location ``/mnt/datastore/store1``:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
||||||
.. code-block:: console
|
.. code-block:: console
|
||||||
|
|
||||||
# proxmox-backup-manager disk fs create store1 --disk sdd --filesystem ext4 --add-datastore true
|
# proxmox-backup-manager disk fs create store1 --disk sdd --filesystem ext4 --add-datastore true
|
||||||
create datastore 'store1' on disk sdd
|
|
||||||
Percentage done: 1
|
|
||||||
...
|
|
||||||
Percentage done: 99
|
|
||||||
TASK OK
|
|
||||||
|
|
||||||
.. image:: images/screenshots/pbs-gui-disks-zfs-create.png
|
.. image:: images/screenshots/pbs-gui-disks-zfs-create.png
|
||||||
:width: 230
|
|
||||||
:align: right
|
:align: right
|
||||||
:alt: Create a directory
|
:alt: Create a directory
|
||||||
|
|
||||||
@ -214,15 +204,9 @@ You can also create a ``zpool`` with various raid levels from **Administration
|
|||||||
below creates a mirrored ``zpool`` using two disks (``sdb`` & ``sdc``) and
|
below creates a mirrored ``zpool`` using two disks (``sdb`` & ``sdc``) and
|
||||||
mounts it on the root directory (default):
|
mounts it on the root directory (default):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
||||||
.. code-block:: console
|
.. code-block:: console
|
||||||
|
|
||||||
# proxmox-backup-manager disk zpool create zpool1 --devices sdb,sdc --raidlevel mirror
|
# proxmox-backup-manager disk zpool create zpool1 --devices sdb,sdc --raidlevel mirror
|
||||||
create Mirror zpool 'zpool1' on devices 'sdb,sdc'
|
|
||||||
# "zpool" "create" "-o" "ashift=12" "zpool1" "mirror" "sdb" "sdc"
|
|
||||||
|
|
||||||
TASK OK
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
You can also pass the ``--add-datastore`` parameter here, to automatically
|
You can also pass the ``--add-datastore`` parameter here, to automatically
|
||||||
@ -243,7 +227,6 @@ Datastore Configuration
|
|||||||
~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
.. image:: images/screenshots/pbs-gui-datastore.png
|
.. image:: images/screenshots/pbs-gui-datastore.png
|
||||||
:width: 230
|
|
||||||
:align: right
|
:align: right
|
||||||
:alt: Datastore Overview
|
:alt: Datastore Overview
|
||||||
|
|
||||||
@ -254,12 +237,11 @@ settings of how many backup snapshots for each interval of ``hourly``,
|
|||||||
``daily``, ``weekly``, ``monthly``, ``yearly`` as well as a time-independent
|
``daily``, ``weekly``, ``monthly``, ``yearly`` as well as a time-independent
|
||||||
number of backups to keep in that store. :ref:`Pruning <pruning>` and
|
number of backups to keep in that store. :ref:`Pruning <pruning>` and
|
||||||
:ref:`garbage collection <garbage-collection>` can also be configured to run
|
:ref:`garbage collection <garbage-collection>` can also be configured to run
|
||||||
periodically based on a configured :term:`schedule` per datastore.
|
periodically based on a configured schedule (see :ref:`calendar-events`) per datastore.
|
||||||
|
|
||||||
Creating a Datastore
|
Creating a Datastore
|
||||||
^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
.. image:: images/screenshots/pbs-gui-datastore-create-general.png
|
.. image:: images/screenshots/pbs-gui-datastore-create-general.png
|
||||||
:width: 230
|
|
||||||
:align: right
|
:align: right
|
||||||
:alt: Create a datastore
|
:alt: Create a datastore
|
||||||
|
|
||||||
@ -382,7 +364,6 @@ User Management
|
|||||||
~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
.. image:: images/screenshots/pbs-gui-user-management.png
|
.. image:: images/screenshots/pbs-gui-user-management.png
|
||||||
:width: 230
|
|
||||||
:align: right
|
:align: right
|
||||||
:alt: User management
|
:alt: User management
|
||||||
|
|
||||||
@ -411,7 +392,6 @@ users:
|
|||||||
└─────────────┴────────┴────────┴───────────┴──────────┴────────────────┴────────────────────┘
|
└─────────────┴────────┴────────┴───────────┴──────────┴────────────────┴────────────────────┘
|
||||||
|
|
||||||
.. image:: images/screenshots/pbs-gui-user-management-add-user.png
|
.. image:: images/screenshots/pbs-gui-user-management-add-user.png
|
||||||
:width: 230
|
|
||||||
:align: right
|
:align: right
|
||||||
:alt: Add a new user
|
:alt: Add a new user
|
||||||
|
|
||||||
@ -503,7 +483,6 @@ following roles exist:
|
|||||||
**RemoteSyncOperator**
|
**RemoteSyncOperator**
|
||||||
Is allowed to read data from a remote.
|
Is allowed to read data from a remote.
|
||||||
|
|
||||||
:width: 230
|
|
||||||
:align: right
|
:align: right
|
||||||
:alt: Add permissions for user
|
:alt: Add permissions for user
|
||||||
|
|
||||||
@ -571,7 +550,6 @@ To get a list of available interfaces, use the following command:
|
|||||||
└───────┴────────┴───────────┴────────┴─────────────┴──────────────┴──────────────┘
|
└───────┴────────┴───────────┴────────┴─────────────┴──────────────┴──────────────┘
|
||||||
|
|
||||||
.. image:: images/screenshots/pbs-gui-network-create-bond.png
|
.. image:: images/screenshots/pbs-gui-network-create-bond.png
|
||||||
:width: 230
|
|
||||||
:align: right
|
:align: right
|
||||||
:alt: Add a network interface
|
:alt: Add a network interface
|
||||||
|
|
||||||
@ -632,7 +610,6 @@ installation, from which you can `sync` datastores to a local datastore with a
|
|||||||
|
|
||||||
.. image:: images/screenshots/pbs-gui-remote-add.png
|
.. image:: images/screenshots/pbs-gui-remote-add.png
|
||||||
.. image:: images/screenshots/pbs-gui-permissions-add.png
|
.. image:: images/screenshots/pbs-gui-permissions-add.png
|
||||||
:width: 230
|
|
||||||
:align: right
|
:align: right
|
||||||
:alt: Add a remote
|
:alt: Add a remote
|
||||||
|
|
||||||
@ -667,18 +644,18 @@ Use the ``list``, ``show``, ``update``, ``remove`` subcommands of
|
|||||||
└──────┴──────────────┴──────────┴───────────────────────────────────────────┴─────────┘
|
└──────┴──────────────┴──────────┴───────────────────────────────────────────┴─────────┘
|
||||||
# proxmox-backup-manager remote remove pbs2
|
# proxmox-backup-manager remote remove pbs2
|
||||||
|
|
||||||
|
.. _syncjobs:
|
||||||
|
|
||||||
Sync Jobs
|
Sync Jobs
|
||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
|
|
||||||
.. image:: images/screenshots/pbs-gui-syncjob-add.png
|
.. image:: images/screenshots/pbs-gui-syncjob-add.png
|
||||||
:width: 230
|
|
||||||
:align: right
|
:align: right
|
||||||
:alt: Add a remote
|
:alt: Add a remote
|
||||||
|
|
||||||
Sync jobs are configured to pull the contents of a datastore on a **Remote** to a
|
Sync jobs are configured to pull the contents of a datastore on a **Remote** to a
|
||||||
local datastore. You can either start a sync job manually on the GUI or
|
local datastore. You can either start a sync job manually on the GUI or
|
||||||
provide it with a :term:`schedule` to run regularly. You can manage sync jobs
|
provide it with a schedule (see :ref:`calendar-events`) to run regularly. You can manage sync jobs
|
||||||
under **Configuration -> Sync Jobs** in the web interface, or using the
|
under **Configuration -> Sync Jobs** in the web interface, or using the
|
||||||
``proxmox-backup-manager sync-job`` command:
|
``proxmox-backup-manager sync-job`` command:
|
||||||
|
|
||||||
|
100
docs/calendarevents.rst
Normal file
100
docs/calendarevents.rst
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
|
||||||
|
.. _calendar-events:
|
||||||
|
|
||||||
|
Calendar Events
|
||||||
|
===============
|
||||||
|
|
||||||
|
Introduction and Format
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Certain tasks, for example pruning and garbage collection, need to be
|
||||||
|
performed on a regular basis. Proxmox Backup Server uses a format inspired
|
||||||
|
by the systemd Time and Date Specification (see `systemd.time manpage`_)
|
||||||
|
called `calendar events` for its schedules.
|
||||||
|
|
||||||
|
`Calendar events` are expressions to specify one or more points in time.
|
||||||
|
They are mostly compatible with systemds calendar events.
|
||||||
|
|
||||||
|
The general format is as follows:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
:caption: Calendar event
|
||||||
|
|
||||||
|
[WEEKDAY] [[YEARS-]MONTHS-DAYS] [HOURS:MINUTES[:SECONDS]]
|
||||||
|
|
||||||
|
Note that there either has to be at least a weekday, date or time part.
|
||||||
|
If the weekday or date part is omitted, all (week)days are included.
|
||||||
|
If the time part is omitted, the time 00:00:00 is implied.
|
||||||
|
(e.g. '2020-01-01' refers to '2020-01-01 00:00:00')
|
||||||
|
|
||||||
|
Weekdays are specified with the abbreviated english version:
|
||||||
|
`mon, tue, wed, thu, fri, sat, sun`.
|
||||||
|
|
||||||
|
Each field can contain multiple values in the following formats:
|
||||||
|
|
||||||
|
* comma-separated: e.g., 01,02,03
|
||||||
|
* as a range: e.g., 01..10
|
||||||
|
* as a repetition: e.g, 05/10 (means starting at 5 every 10)
|
||||||
|
* and a combination of the above: e.g., 01,05..10,12/02
|
||||||
|
* or a `*` for every possible value: e.g., \*:00
|
||||||
|
|
||||||
|
There are some special values that have specific meaning:
|
||||||
|
|
||||||
|
================================= ==============================
|
||||||
|
Value Syntax
|
||||||
|
================================= ==============================
|
||||||
|
`minutely` `*-*-* *:*:00`
|
||||||
|
`hourly` `*-*-* *:00:00`
|
||||||
|
`daily` `*-*-* 00:00:00`
|
||||||
|
`weekly` `mon *-*-* 00:00:00`
|
||||||
|
`monthly` `*-*-01 00:00:00`
|
||||||
|
`yearly` or `annualy` `*-01-01 00:00:00`
|
||||||
|
`quarterly` `*-01,04,07,10-01 00:00:00`
|
||||||
|
`semiannually` or `semi-annually` `*-01,07-01 00:00:00`
|
||||||
|
================================= ==============================
|
||||||
|
|
||||||
|
|
||||||
|
Here is a table with some useful examples:
|
||||||
|
|
||||||
|
======================== ============================= ===================================
|
||||||
|
Example Alternative Explanation
|
||||||
|
======================== ============================= ===================================
|
||||||
|
`mon,tue,wed,thu,fri` `mon..fri` Every working day at 00:00
|
||||||
|
`sat,sun` `sat..sun` Only on weekends at 00:00
|
||||||
|
`mon,wed,fri` -- Monday, Wednesday, Friday at 00:00
|
||||||
|
`12:05` -- Every day at 12:05 PM
|
||||||
|
`*:00/5` `0/1:0/5` Every five minutes
|
||||||
|
`mon..wed *:30/10` `mon,tue,wed *:30/10` Monday, Tuesday, Wednesday 30, 40 and 50 minutes after every full hour
|
||||||
|
`mon..fri 8..17,22:0/15` -- Every working day every 15 minutes between 8 AM and 6 PM and between 10 PM and 11 PM
|
||||||
|
`fri 12..13:5/20` `fri 12,13:5/20` Friday at 12:05, 12:25, 12:45, 13:05, 13:25 and 13:45
|
||||||
|
`12,14,16,18,20,22:5` `12/2:5` Every day starting at 12:05 until 22:05, every 2 hours
|
||||||
|
`*:*` `0/1:0/1` Every minute (minimum interval)
|
||||||
|
`*-05` -- On the 5th day of every Month
|
||||||
|
`Sat *-1..7 15:00` -- First Saturday each Month at 15:00
|
||||||
|
`2015-10-21` -- 21st October 2015 at 00:00
|
||||||
|
======================== ============================= ===================================
|
||||||
|
|
||||||
|
|
||||||
|
Differences to systemd
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Not all features of systemd calendar events are implemented:
|
||||||
|
|
||||||
|
* no unix timestamps (e.g. `@12345`): instead use date and time to specify
|
||||||
|
a specific point in time
|
||||||
|
* no timezone: all schedules use the set timezone on the server
|
||||||
|
* no sub-second resolution
|
||||||
|
* no reverse day syntax (e.g. 2020-03~01)
|
||||||
|
* no repetition of ranges (e.g. 1..10/2)
|
||||||
|
|
||||||
|
Notes on scheduling
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
In `Proxmox Backup`_ scheduling for most tasks is done in the
|
||||||
|
`proxmox-backup-proxy`. This daemon checks all job schedules
|
||||||
|
if they are due every minute. This means that even if
|
||||||
|
`calendar events` can contain seconds, it will only be checked
|
||||||
|
once a minute.
|
||||||
|
|
||||||
|
Also, all schedules will be checked against the timezone set
|
||||||
|
in the `Proxmox Backup`_ server.
|
31
docs/conf.py
31
docs/conf.py
@ -74,7 +74,7 @@ rst_epilog = epilog_file.read()
|
|||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = 'Proxmox Backup'
|
project = 'Proxmox Backup'
|
||||||
copyright = '2019-2020, Proxmox Support Team'
|
copyright = '2019-2020, Proxmox Server Solutions GmbH'
|
||||||
author = 'Proxmox Support Team'
|
author = 'Proxmox Support Team'
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
@ -148,7 +148,7 @@ pygments_style = 'sphinx'
|
|||||||
# keep_warnings = False
|
# keep_warnings = False
|
||||||
|
|
||||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||||
todo_include_todos = True
|
todo_include_todos = not tags.has('release')
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTML output ----------------------------------------------
|
# -- Options for HTML output ----------------------------------------------
|
||||||
@ -156,13 +156,32 @@ todo_include_todos = True
|
|||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
# a list of builtin themes.
|
# a list of builtin themes.
|
||||||
#
|
#
|
||||||
html_theme = 'sphinxdoc'
|
html_theme = 'alabaster'
|
||||||
|
|
||||||
# Theme options are theme-specific and customize the look and feel of a theme
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
# further. For a list of options available for each theme, see the
|
# further. For a list of options available for each theme, see the
|
||||||
# documentation.
|
# documentation.
|
||||||
#
|
#
|
||||||
# html_theme_options = {}
|
html_theme_options = {
|
||||||
|
'fixed_sidebar': True,
|
||||||
|
#'sidebar_includehidden': False,
|
||||||
|
'sidebar_collapse': False, # FIXME: documented, but does not works?!
|
||||||
|
'show_relbar_bottom': True, # FIXME: documented, but does not works?!
|
||||||
|
'show_powered_by': False,
|
||||||
|
|
||||||
|
'logo': 'proxmox-logo.svg',
|
||||||
|
'logo_name': True, # show project name below logo
|
||||||
|
#'logo_text_align': 'center',
|
||||||
|
#'description': 'Fast, Secure & Efficient.',
|
||||||
|
|
||||||
|
'sidebar_width': '300px',
|
||||||
|
'page_width': '1280px',
|
||||||
|
# font styles
|
||||||
|
'head_font_family': 'Lato, sans-serif',
|
||||||
|
'caption_font_family': 'Lato, sans-serif',
|
||||||
|
'caption_font_size': '20px',
|
||||||
|
'font_family': 'Open Sans, sans-serif',
|
||||||
|
}
|
||||||
|
|
||||||
# Add any paths that contain custom themes here, relative to this directory.
|
# Add any paths that contain custom themes here, relative to this directory.
|
||||||
# html_theme_path = []
|
# html_theme_path = []
|
||||||
@ -179,7 +198,7 @@ html_theme = 'sphinxdoc'
|
|||||||
# The name of an image file (relative to this directory) to place at the top
|
# The name of an image file (relative to this directory) to place at the top
|
||||||
# of the sidebar.
|
# of the sidebar.
|
||||||
#
|
#
|
||||||
html_logo = 'images/proxmox-logo.svg'
|
#html_logo = 'images/proxmox-logo.svg' # replaced by html_theme_options.logo
|
||||||
|
|
||||||
# The name of an image file (relative to this directory) to use as a favicon of
|
# The name of an image file (relative to this directory) to use as a favicon of
|
||||||
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||||
@ -232,7 +251,7 @@ html_static_path = ['_static']
|
|||||||
|
|
||||||
# If true, links to the reST sources are added to the pages.
|
# If true, links to the reST sources are added to the pages.
|
||||||
#
|
#
|
||||||
# html_show_sourcelink = True
|
html_show_sourcelink = False
|
||||||
|
|
||||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||||
#
|
#
|
||||||
|
15
docs/custom.css
Normal file
15
docs/custom.css
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
div.sphinxsidebar {
|
||||||
|
height: calc(100% - 20px);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1.logo-name {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.body img {
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
@ -38,3 +38,6 @@
|
|||||||
.. _RFC3399: https://tools.ietf.org/html/rfc3339
|
.. _RFC3399: https://tools.ietf.org/html/rfc3339
|
||||||
.. _UTC: https://en.wikipedia.org/wiki/Coordinated_Universal_Time
|
.. _UTC: https://en.wikipedia.org/wiki/Coordinated_Universal_Time
|
||||||
.. _ISO Week date: https://en.wikipedia.org/wiki/ISO_week_date
|
.. _ISO Week date: https://en.wikipedia.org/wiki/ISO_week_date
|
||||||
|
|
||||||
|
.. _systemd.time manpage: https://manpages.debian.org/buster/systemd/systemd.time.7.en.html
|
||||||
|
|
||||||
|
71
docs/faq.rst
Normal file
71
docs/faq.rst
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
FAQ
|
||||||
|
===
|
||||||
|
|
||||||
|
What distribution is Proxmox Backup Server (PBS) based on?
|
||||||
|
----------------------------------------------------------
|
||||||
|
|
||||||
|
Proxmox Backup Server is based on `Debian GNU/Linux <https://www.debian.org/>`_.
|
||||||
|
|
||||||
|
|
||||||
|
Which platforms are supported as a backup source (client)?
|
||||||
|
----------------------------------------------------------
|
||||||
|
|
||||||
|
The client tool works on most modern Linux systems, meaning you are not limited
|
||||||
|
to Debian-based backups.
|
||||||
|
|
||||||
|
|
||||||
|
Will Proxmox Backup Server run on a 32-bit processor?
|
||||||
|
-----------------------------------------------------
|
||||||
|
|
||||||
|
Proxmox Backup Server only supports 64-bit CPUs (AMD or Intel). There are no
|
||||||
|
future plans to support 32-bit processors.
|
||||||
|
|
||||||
|
|
||||||
|
How long will my Proxmox Backup Server version be supported?
|
||||||
|
------------------------------------------------------------
|
||||||
|
|
||||||
|
+-----------------------+--------------------+---------------+------------+--------------------+
|
||||||
|
|Proxmox Backup Version | Debian Version | First Release | Debian EOL | Proxmox Backup EOL |
|
||||||
|
+=======================+====================+===============+============+====================+
|
||||||
|
|Proxmox Backup 1.x | Debian 10 (Buster) | tba | tba | tba |
|
||||||
|
+-----------------------+--------------------+---------------+------------+--------------------+
|
||||||
|
|
||||||
|
|
||||||
|
Can I copy or synchronize my datastore to another location?
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
Proxmox Backup Server allows you to copy or synchroize datastores to other
|
||||||
|
locations, through the use of *Remotes* and *Sync Jobs*. *Remote* is the term
|
||||||
|
given to a separate server, which has a datastore that can be synced to a local store.
|
||||||
|
A *Sync Job* is the process which is used to pull the contents of a datastore from
|
||||||
|
a *Remote* to a local datastore.
|
||||||
|
|
||||||
|
|
||||||
|
Can Proxmox Backup Server verify data integrity of a backup archive?
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
|
||||||
|
Proxmox Backup Server uses a built-in SHA-256 checksum algorithm, to ensure
|
||||||
|
data integrity. Within each backup, a manifest file (index.json) is created,
|
||||||
|
which contains a list of all the backup files, along with their sizes and
|
||||||
|
checksums. This manifest file is used to verify the integrity of each backup.
|
||||||
|
|
||||||
|
|
||||||
|
When backing up to remote servers, do I have to trust the remote server?
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Proxmox Backup Server supports client-side encryption, meaning your data is
|
||||||
|
encrypted before it reaches the server. Thus, in the event that an attacker
|
||||||
|
gains access to the server, they will not be able to read the data.
|
||||||
|
|
||||||
|
.. note:: Encryption is not enabled by default. To set up encryption, see the
|
||||||
|
`Encryption
|
||||||
|
<https://pbs.proxmox.com/docs/administration-guide.html#encryption>`_ section
|
||||||
|
of the Proxmox Backup Server Administration Guide.
|
||||||
|
|
||||||
|
|
||||||
|
Is the backup incremental/deduplicated?
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
With Proxmox Backup Server, backups are sent incremental and data is
|
||||||
|
deduplicated on the server.
|
||||||
|
This minimizes both the storage consumed and the network impact.
|
@ -51,14 +51,3 @@ Glossary
|
|||||||
A remote Proxmox Backup Server installation and credentials for a user on it.
|
A remote Proxmox Backup Server installation and credentials for a user on it.
|
||||||
You can pull datastores from a remote to a local datastore in order to
|
You can pull datastores from a remote to a local datastore in order to
|
||||||
have redundant backups.
|
have redundant backups.
|
||||||
|
|
||||||
Schedule
|
|
||||||
|
|
||||||
Certain tasks, for example pruning and garbage collection, need to be
|
|
||||||
performed on a regular basis. Proxmox Backup Server uses a subset of the
|
|
||||||
`systemd Time and Date Specification
|
|
||||||
<https://www.freedesktop.org/software/systemd/man/systemd.time.html#>`_.
|
|
||||||
The subset currently supports time of day specifications and weekdays, in
|
|
||||||
addition to the shorthand expressions 'minutely', 'hourly', 'daily'.
|
|
||||||
There is no support for specifying timezones, the tasks are run in the
|
|
||||||
timezone configured on the server.
|
|
||||||
|
@ -24,6 +24,7 @@ in the section entitled "GNU Free Documentation License".
|
|||||||
installation.rst
|
installation.rst
|
||||||
administration-guide.rst
|
administration-guide.rst
|
||||||
sysadmin.rst
|
sysadmin.rst
|
||||||
|
faq.rst
|
||||||
|
|
||||||
.. raw:: latex
|
.. raw:: latex
|
||||||
|
|
||||||
@ -36,6 +37,7 @@ in the section entitled "GNU Free Documentation License".
|
|||||||
command-syntax.rst
|
command-syntax.rst
|
||||||
file-formats.rst
|
file-formats.rst
|
||||||
backup-protocol.rst
|
backup-protocol.rst
|
||||||
|
calendarevents.rst
|
||||||
glossary.rst
|
glossary.rst
|
||||||
GFDL.rst
|
GFDL.rst
|
||||||
|
|
||||||
@ -49,4 +51,3 @@ in the section entitled "GNU Free Documentation License".
|
|||||||
|
|
||||||
|
|
||||||
* :ref:`genindex`
|
* :ref:`genindex`
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ async fn upload_speed() -> Result<f64, Error> {
|
|||||||
|
|
||||||
let backup_time = chrono::Utc::now();
|
let backup_time = chrono::Utc::now();
|
||||||
|
|
||||||
let client = BackupWriter::start(client, None, datastore, "host", "speedtest", backup_time, false).await?;
|
let client = BackupWriter::start(client, None, datastore, "host", "speedtest", backup_time, false, true).await?;
|
||||||
|
|
||||||
println!("start upload speed test");
|
println!("start upload speed test");
|
||||||
let res = client.upload_speedtest(true).await?;
|
let res = client.upload_speedtest(true).await?;
|
||||||
|
@ -230,7 +230,7 @@ pub fn list_snapshot_files(
|
|||||||
|
|
||||||
let datastore = DataStore::lookup_datastore(&store)?;
|
let datastore = DataStore::lookup_datastore(&store)?;
|
||||||
|
|
||||||
let snapshot = BackupDir::new(backup_type, backup_id, backup_time);
|
let snapshot = BackupDir::new(backup_type, backup_id, backup_time)?;
|
||||||
|
|
||||||
let allowed = (user_privs & (PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_READ)) != 0;
|
let allowed = (user_privs & (PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_READ)) != 0;
|
||||||
if !allowed { check_backup_owner(&datastore, snapshot.group(), &userid)?; }
|
if !allowed { check_backup_owner(&datastore, snapshot.group(), &userid)?; }
|
||||||
@ -280,7 +280,7 @@ fn delete_snapshot(
|
|||||||
let user_info = CachedUserInfo::new()?;
|
let user_info = CachedUserInfo::new()?;
|
||||||
let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
|
let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
|
||||||
|
|
||||||
let snapshot = BackupDir::new(backup_type, backup_id, backup_time);
|
let snapshot = BackupDir::new(backup_type, backup_id, backup_time)?;
|
||||||
|
|
||||||
let datastore = DataStore::lookup_datastore(&store)?;
|
let datastore = DataStore::lookup_datastore(&store)?;
|
||||||
|
|
||||||
@ -490,7 +490,7 @@ pub fn verify(
|
|||||||
match (backup_type, backup_id, backup_time) {
|
match (backup_type, backup_id, backup_time) {
|
||||||
(Some(backup_type), Some(backup_id), Some(backup_time)) => {
|
(Some(backup_type), Some(backup_id), Some(backup_time)) => {
|
||||||
worker_id = format!("{}_{}_{}_{:08X}", store, backup_type, backup_id, backup_time);
|
worker_id = format!("{}_{}_{}_{:08X}", store, backup_type, backup_id, backup_time);
|
||||||
let dir = BackupDir::new(backup_type, backup_id, backup_time);
|
let dir = BackupDir::new(backup_type, backup_id, backup_time)?;
|
||||||
backup_dir = Some(dir);
|
backup_dir = Some(dir);
|
||||||
}
|
}
|
||||||
(Some(backup_type), Some(backup_id), None) => {
|
(Some(backup_type), Some(backup_id), None) => {
|
||||||
@ -897,7 +897,7 @@ fn download_file(
|
|||||||
let backup_id = tools::required_string_param(¶m, "backup-id")?;
|
let backup_id = tools::required_string_param(¶m, "backup-id")?;
|
||||||
let backup_time = tools::required_integer_param(¶m, "backup-time")?;
|
let backup_time = tools::required_integer_param(¶m, "backup-time")?;
|
||||||
|
|
||||||
let backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
|
let backup_dir = BackupDir::new(backup_type, backup_id, backup_time)?;
|
||||||
|
|
||||||
let allowed = (user_privs & PRIV_DATASTORE_READ) != 0;
|
let allowed = (user_privs & PRIV_DATASTORE_READ) != 0;
|
||||||
if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; }
|
if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; }
|
||||||
@ -970,7 +970,7 @@ fn download_file_decoded(
|
|||||||
let backup_id = tools::required_string_param(¶m, "backup-id")?;
|
let backup_id = tools::required_string_param(¶m, "backup-id")?;
|
||||||
let backup_time = tools::required_integer_param(¶m, "backup-time")?;
|
let backup_time = tools::required_integer_param(¶m, "backup-time")?;
|
||||||
|
|
||||||
let backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
|
let backup_dir = BackupDir::new(backup_type, backup_id, backup_time)?;
|
||||||
|
|
||||||
let allowed = (user_privs & PRIV_DATASTORE_READ) != 0;
|
let allowed = (user_privs & PRIV_DATASTORE_READ) != 0;
|
||||||
if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; }
|
if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; }
|
||||||
@ -1083,7 +1083,7 @@ fn upload_backup_log(
|
|||||||
let backup_id = tools::required_string_param(¶m, "backup-id")?;
|
let backup_id = tools::required_string_param(¶m, "backup-id")?;
|
||||||
let backup_time = tools::required_integer_param(¶m, "backup-time")?;
|
let backup_time = tools::required_integer_param(¶m, "backup-time")?;
|
||||||
|
|
||||||
let backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
|
let backup_dir = BackupDir::new(backup_type, backup_id, backup_time)?;
|
||||||
|
|
||||||
let userid: Userid = rpcenv.get_user().unwrap().parse()?;
|
let userid: Userid = rpcenv.get_user().unwrap().parse()?;
|
||||||
check_backup_owner(&datastore, backup_dir.group(), &userid)?;
|
check_backup_owner(&datastore, backup_dir.group(), &userid)?;
|
||||||
@ -1159,7 +1159,7 @@ fn catalog(
|
|||||||
let user_info = CachedUserInfo::new()?;
|
let user_info = CachedUserInfo::new()?;
|
||||||
let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
|
let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
|
||||||
|
|
||||||
let backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
|
let backup_dir = BackupDir::new(backup_type, backup_id, backup_time)?;
|
||||||
|
|
||||||
let allowed = (user_privs & PRIV_DATASTORE_READ) != 0;
|
let allowed = (user_privs & PRIV_DATASTORE_READ) != 0;
|
||||||
if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; }
|
if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; }
|
||||||
@ -1276,7 +1276,7 @@ fn pxar_file_download(
|
|||||||
let backup_id = tools::required_string_param(¶m, "backup-id")?;
|
let backup_id = tools::required_string_param(¶m, "backup-id")?;
|
||||||
let backup_time = tools::required_integer_param(¶m, "backup-time")?;
|
let backup_time = tools::required_integer_param(¶m, "backup-time")?;
|
||||||
|
|
||||||
let backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
|
let backup_dir = BackupDir::new(backup_type, backup_id, backup_time)?;
|
||||||
|
|
||||||
let allowed = (user_privs & PRIV_DATASTORE_READ) != 0;
|
let allowed = (user_privs & PRIV_DATASTORE_READ) != 0;
|
||||||
if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; }
|
if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; }
|
||||||
@ -1417,7 +1417,7 @@ fn get_notes(
|
|||||||
let user_info = CachedUserInfo::new()?;
|
let user_info = CachedUserInfo::new()?;
|
||||||
let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
|
let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
|
||||||
|
|
||||||
let backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
|
let backup_dir = BackupDir::new(backup_type, backup_id, backup_time)?;
|
||||||
|
|
||||||
let allowed = (user_privs & PRIV_DATASTORE_READ) != 0;
|
let allowed = (user_privs & PRIV_DATASTORE_READ) != 0;
|
||||||
if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; }
|
if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; }
|
||||||
@ -1470,7 +1470,7 @@ fn set_notes(
|
|||||||
let user_info = CachedUserInfo::new()?;
|
let user_info = CachedUserInfo::new()?;
|
||||||
let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
|
let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
|
||||||
|
|
||||||
let backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
|
let backup_dir = BackupDir::new(backup_type, backup_id, backup_time)?;
|
||||||
|
|
||||||
let allowed = (user_privs & PRIV_DATASTORE_READ) != 0;
|
let allowed = (user_privs & PRIV_DATASTORE_READ) != 0;
|
||||||
if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; }
|
if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; }
|
||||||
|
@ -114,7 +114,7 @@ async move {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let last_backup = BackupInfo::last_backup(&datastore.base_path(), &backup_group, true).unwrap_or(None);
|
let last_backup = BackupInfo::last_backup(&datastore.base_path(), &backup_group, true).unwrap_or(None);
|
||||||
let backup_dir = BackupDir::new_with_group(backup_group.clone(), backup_time);
|
let backup_dir = BackupDir::new_with_group(backup_group.clone(), backup_time)?;
|
||||||
|
|
||||||
let _last_guard = if let Some(last) = &last_backup {
|
let _last_guard = if let Some(last) = &last_backup {
|
||||||
if backup_dir.backup_time() <= last.backup_dir.backup_time() {
|
if backup_dir.backup_time() <= last.backup_dir.backup_time() {
|
||||||
@ -159,6 +159,7 @@ async move {
|
|||||||
let window_size = 32*1024*1024; // max = (1 << 31) - 2
|
let window_size = 32*1024*1024; // max = (1 << 31) - 2
|
||||||
http.http2_initial_stream_window_size(window_size);
|
http.http2_initial_stream_window_size(window_size);
|
||||||
http.http2_initial_connection_window_size(window_size);
|
http.http2_initial_connection_window_size(window_size);
|
||||||
|
http.http2_max_frame_size(4*1024*1024);
|
||||||
|
|
||||||
http.serve_connection(conn, service)
|
http.serve_connection(conn, service)
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
|
@ -83,7 +83,7 @@ fn upgrade_to_backup_reader_protocol(
|
|||||||
|
|
||||||
let env_type = rpcenv.env_type();
|
let env_type = rpcenv.env_type();
|
||||||
|
|
||||||
let backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
|
let backup_dir = BackupDir::new(backup_type, backup_id, backup_time)?;
|
||||||
let path = datastore.base_path();
|
let path = datastore.base_path();
|
||||||
|
|
||||||
//let files = BackupInfo::list_files(&path, &backup_dir)?;
|
//let files = BackupInfo::list_files(&path, &backup_dir)?;
|
||||||
@ -121,6 +121,7 @@ fn upgrade_to_backup_reader_protocol(
|
|||||||
let window_size = 32*1024*1024; // max = (1 << 31) - 2
|
let window_size = 32*1024*1024; // max = (1 << 31) - 2
|
||||||
http.http2_initial_stream_window_size(window_size);
|
http.http2_initial_stream_window_size(window_size);
|
||||||
http.http2_initial_connection_window_size(window_size);
|
http.http2_initial_connection_window_size(window_size);
|
||||||
|
http.http2_max_frame_size(4*1024*1024);
|
||||||
|
|
||||||
http.serve_connection(conn, service)
|
http.serve_connection(conn, service)
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
|
@ -2,9 +2,10 @@ use crate::tools;
|
|||||||
|
|
||||||
use anyhow::{bail, format_err, Error};
|
use anyhow::{bail, format_err, Error};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::os::unix::io::RawFd;
|
use std::os::unix::io::RawFd;
|
||||||
|
|
||||||
use chrono::{DateTime, TimeZone, SecondsFormat, Utc};
|
use chrono::{DateTime, LocalResult, TimeZone, SecondsFormat, Utc};
|
||||||
|
|
||||||
use std::path::{PathBuf, Path};
|
use std::path::{PathBuf, Path};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
@ -106,7 +107,7 @@ impl BackupGroup {
|
|||||||
if file_type != nix::dir::Type::Directory { return Ok(()); }
|
if file_type != nix::dir::Type::Directory { return Ok(()); }
|
||||||
|
|
||||||
let dt = backup_time.parse::<DateTime<Utc>>()?;
|
let dt = backup_time.parse::<DateTime<Utc>>()?;
|
||||||
let backup_dir = BackupDir::new(self.backup_type.clone(), self.backup_id.clone(), dt.timestamp());
|
let backup_dir = BackupDir::new(self.backup_type.clone(), self.backup_id.clone(), dt.timestamp())?;
|
||||||
let files = list_backup_files(l2_fd, backup_time)?;
|
let files = list_backup_files(l2_fd, backup_time)?;
|
||||||
|
|
||||||
list.push(BackupInfo { backup_dir, files });
|
list.push(BackupInfo { backup_dir, files });
|
||||||
@ -208,19 +209,22 @@ pub struct BackupDir {
|
|||||||
|
|
||||||
impl BackupDir {
|
impl BackupDir {
|
||||||
|
|
||||||
pub fn new<T, U>(backup_type: T, backup_id: U, timestamp: i64) -> Self
|
pub fn new<T, U>(backup_type: T, backup_id: U, timestamp: i64) -> Result<Self, Error>
|
||||||
where
|
where
|
||||||
T: Into<String>,
|
T: Into<String>,
|
||||||
U: Into<String>,
|
U: Into<String>,
|
||||||
{
|
{
|
||||||
// Note: makes sure that nanoseconds is 0
|
let group = BackupGroup::new(backup_type.into(), backup_id.into());
|
||||||
Self {
|
BackupDir::new_with_group(group, timestamp)
|
||||||
group: BackupGroup::new(backup_type.into(), backup_id.into()),
|
|
||||||
backup_time: Utc.timestamp(timestamp, 0),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
pub fn new_with_group(group: BackupGroup, timestamp: i64) -> Self {
|
|
||||||
Self { group, backup_time: Utc.timestamp(timestamp, 0) }
|
pub fn new_with_group(group: BackupGroup, timestamp: i64) -> Result<Self, Error> {
|
||||||
|
let backup_time = match Utc.timestamp_opt(timestamp, 0) {
|
||||||
|
LocalResult::Single(time) => time,
|
||||||
|
_ => bail!("can't create BackupDir with invalid backup time {}", timestamp),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self { group, backup_time })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn group(&self) -> &BackupGroup {
|
pub fn group(&self) -> &BackupGroup {
|
||||||
@ -257,7 +261,7 @@ impl std::str::FromStr for BackupDir {
|
|||||||
|
|
||||||
let group = BackupGroup::new(cap.get(1).unwrap().as_str(), cap.get(2).unwrap().as_str());
|
let group = BackupGroup::new(cap.get(1).unwrap().as_str(), cap.get(2).unwrap().as_str());
|
||||||
let backup_time = cap.get(3).unwrap().as_str().parse::<DateTime<Utc>>()?;
|
let backup_time = cap.get(3).unwrap().as_str().parse::<DateTime<Utc>>()?;
|
||||||
Ok(BackupDir::from((group, backup_time.timestamp())))
|
BackupDir::try_from((group, backup_time.timestamp()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,9 +274,11 @@ impl std::fmt::Display for BackupDir {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<(BackupGroup, i64)> for BackupDir {
|
impl TryFrom<(BackupGroup, i64)> for BackupDir {
|
||||||
fn from((group, timestamp): (BackupGroup, i64)) -> Self {
|
type Error = Error;
|
||||||
Self { group, backup_time: Utc.timestamp(timestamp, 0) }
|
|
||||||
|
fn try_from((group, timestamp): (BackupGroup, i64)) -> Result<Self, Error> {
|
||||||
|
BackupDir::new_with_group(group, timestamp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -334,7 +340,7 @@ impl BackupInfo {
|
|||||||
if file_type != nix::dir::Type::Directory { return Ok(()); }
|
if file_type != nix::dir::Type::Directory { return Ok(()); }
|
||||||
|
|
||||||
let dt = backup_time.parse::<DateTime<Utc>>()?;
|
let dt = backup_time.parse::<DateTime<Utc>>()?;
|
||||||
let backup_dir = BackupDir::new(backup_type, backup_id, dt.timestamp());
|
let backup_dir = BackupDir::new(backup_type, backup_id, dt.timestamp())?;
|
||||||
|
|
||||||
let files = list_backup_files(l2_fd, backup_time)?;
|
let files = list_backup_files(l2_fd, backup_time)?;
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ use std::io::{Read, Write, Seek, SeekFrom};
|
|||||||
use std::os::unix::ffi::OsStrExt;
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
|
||||||
use anyhow::{bail, format_err, Error};
|
use anyhow::{bail, format_err, Error};
|
||||||
use chrono::offset::{TimeZone, Local};
|
use chrono::offset::{TimeZone, Local, LocalResult};
|
||||||
|
|
||||||
use pathpatterns::{MatchList, MatchType};
|
use pathpatterns::{MatchList, MatchType};
|
||||||
use proxmox::tools::io::ReadExt;
|
use proxmox::tools::io::ReadExt;
|
||||||
@ -533,17 +533,17 @@ impl <R: Read + Seek> CatalogReader<R> {
|
|||||||
self.dump_dir(&path, pos)?;
|
self.dump_dir(&path, pos)?;
|
||||||
}
|
}
|
||||||
CatalogEntryType::File => {
|
CatalogEntryType::File => {
|
||||||
let dt = Local
|
let mtime_string = match Local.timestamp_opt(mtime as i64, 0) {
|
||||||
.timestamp_opt(mtime as i64, 0)
|
LocalResult::Single(time) => time.to_rfc3339_opts(chrono::SecondsFormat::Secs, false),
|
||||||
.single() // chrono docs say timestamp_opt can only be None or Single!
|
_ => (mtime as i64).to_string(),
|
||||||
.unwrap_or_else(|| Local.timestamp(0, 0));
|
};
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"{} {:?} {} {}",
|
"{} {:?} {} {}",
|
||||||
etype,
|
etype,
|
||||||
path,
|
path,
|
||||||
size,
|
size,
|
||||||
dt.to_rfc3339_opts(chrono::SecondsFormat::Secs, false),
|
mtime_string,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
use anyhow::{bail, Error};
|
use anyhow::{bail, Error};
|
||||||
use chrono::{Local, TimeZone, DateTime};
|
use chrono::{Local, DateTime};
|
||||||
use openssl::hash::MessageDigest;
|
use openssl::hash::MessageDigest;
|
||||||
use openssl::pkcs5::pbkdf2_hmac;
|
use openssl::pkcs5::pbkdf2_hmac;
|
||||||
use openssl::symm::{decrypt_aead, Cipher, Crypter, Mode};
|
use openssl::symm::{decrypt_aead, Cipher, Crypter, Mode};
|
||||||
@ -219,7 +219,7 @@ impl CryptConfig {
|
|||||||
created: DateTime<Local>,
|
created: DateTime<Local>,
|
||||||
) -> Result<Vec<u8>, Error> {
|
) -> Result<Vec<u8>, Error> {
|
||||||
|
|
||||||
let modified = Local.timestamp(Local::now().timestamp(), 0);
|
let modified = Local::now();
|
||||||
let key_config = super::KeyConfig { kdf: None, created, modified, data: self.enc_key.to_vec() };
|
let key_config = super::KeyConfig { kdf: None, created, modified, data: self.enc_key.to_vec() };
|
||||||
let data = serde_json::to_string(&key_config)?.as_bytes().to_vec();
|
let data = serde_json::to_string(&key_config)?.as_bytes().to_vec();
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ use super::chunk_store::*;
|
|||||||
use super::{IndexFile, ChunkReadInfo};
|
use super::{IndexFile, ChunkReadInfo};
|
||||||
use crate::tools::{self, epoch_now_u64};
|
use crate::tools::{self, epoch_now_u64};
|
||||||
|
|
||||||
use chrono::{Local, TimeZone};
|
use chrono::{Local, LocalResult, TimeZone};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::os::unix::io::AsRawFd;
|
use std::os::unix::io::AsRawFd;
|
||||||
@ -150,7 +150,10 @@ impl FixedIndexReader {
|
|||||||
println!("ChunkSize: {}", self.chunk_size);
|
println!("ChunkSize: {}", self.chunk_size);
|
||||||
println!(
|
println!(
|
||||||
"CTime: {}",
|
"CTime: {}",
|
||||||
Local.timestamp(self.ctime as i64, 0).format("%c")
|
match Local.timestamp_opt(self.ctime as i64, 0) {
|
||||||
|
LocalResult::Single(ctime) => ctime.format("%c").to_string(),
|
||||||
|
_ => (self.ctime as i64).to_string(),
|
||||||
|
}
|
||||||
);
|
);
|
||||||
println!("UUID: {:?}", self.uuid);
|
println!("UUID: {:?}", self.uuid);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use anyhow::{bail, format_err, Context, Error};
|
use anyhow::{bail, format_err, Context, Error};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use chrono::{Local, TimeZone, DateTime};
|
use chrono::{Local, DateTime};
|
||||||
|
|
||||||
use proxmox::tools::fs::{file_get_contents, replace_file, CreateOptions};
|
use proxmox::tools::fs::{file_get_contents, replace_file, CreateOptions};
|
||||||
use proxmox::try_block;
|
use proxmox::try_block;
|
||||||
@ -136,7 +136,7 @@ pub fn encrypt_key_with_passphrase(
|
|||||||
enc_data.extend_from_slice(&tag);
|
enc_data.extend_from_slice(&tag);
|
||||||
enc_data.extend_from_slice(&encrypted_key);
|
enc_data.extend_from_slice(&encrypted_key);
|
||||||
|
|
||||||
let created = Local.timestamp(Local::now().timestamp(), 0);
|
let created = Local::now();
|
||||||
|
|
||||||
Ok(KeyConfig {
|
Ok(KeyConfig {
|
||||||
kdf: Some(kdf),
|
kdf: Some(kdf),
|
||||||
|
@ -8,7 +8,7 @@ use std::sync::{Arc, Mutex};
|
|||||||
use std::task::Context;
|
use std::task::Context;
|
||||||
|
|
||||||
use anyhow::{bail, format_err, Error};
|
use anyhow::{bail, format_err, Error};
|
||||||
use chrono::{Local, DateTime, Utc, TimeZone};
|
use chrono::{Local, LocalResult, DateTime, Utc, TimeZone};
|
||||||
use futures::future::FutureExt;
|
use futures::future::FutureExt;
|
||||||
use futures::stream::{StreamExt, TryStreamExt};
|
use futures::stream::{StreamExt, TryStreamExt};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
@ -257,7 +257,11 @@ pub async fn api_datastore_latest_snapshot(
|
|||||||
|
|
||||||
list.sort_unstable_by(|a, b| b.backup_time.cmp(&a.backup_time));
|
list.sort_unstable_by(|a, b| b.backup_time.cmp(&a.backup_time));
|
||||||
|
|
||||||
let backup_time = Utc.timestamp(list[0].backup_time, 0);
|
let backup_time = match Utc.timestamp_opt(list[0].backup_time, 0) {
|
||||||
|
LocalResult::Single(time) => time,
|
||||||
|
_ => bail!("last snapshot of backup group {:?} has invalid timestmap {}.",
|
||||||
|
group.group_path(), list[0].backup_time),
|
||||||
|
};
|
||||||
|
|
||||||
Ok((group.backup_type().to_owned(), group.backup_id().to_owned(), backup_time))
|
Ok((group.backup_type().to_owned(), group.backup_id().to_owned(), backup_time))
|
||||||
}
|
}
|
||||||
@ -373,7 +377,7 @@ async fn list_backup_groups(param: Value) -> Result<Value, Error> {
|
|||||||
|
|
||||||
let render_last_backup = |_v: &Value, record: &Value| -> Result<String, Error> {
|
let render_last_backup = |_v: &Value, record: &Value| -> Result<String, Error> {
|
||||||
let item: GroupListItem = serde_json::from_value(record.to_owned())?;
|
let item: GroupListItem = serde_json::from_value(record.to_owned())?;
|
||||||
let snapshot = BackupDir::new(item.backup_type, item.backup_id, item.last_backup);
|
let snapshot = BackupDir::new(item.backup_type, item.backup_id, item.last_backup)?;
|
||||||
Ok(snapshot.relative_path().to_str().unwrap().to_owned())
|
Ok(snapshot.relative_path().to_str().unwrap().to_owned())
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -444,7 +448,7 @@ async fn list_snapshots(param: Value) -> Result<Value, Error> {
|
|||||||
|
|
||||||
let render_snapshot_path = |_v: &Value, record: &Value| -> Result<String, Error> {
|
let render_snapshot_path = |_v: &Value, record: &Value| -> Result<String, Error> {
|
||||||
let item: SnapshotListItem = serde_json::from_value(record.to_owned())?;
|
let item: SnapshotListItem = serde_json::from_value(record.to_owned())?;
|
||||||
let snapshot = BackupDir::new(item.backup_type, item.backup_id, item.backup_time);
|
let snapshot = BackupDir::new(item.backup_type, item.backup_id, item.backup_time)?;
|
||||||
Ok(snapshot.relative_path().to_str().unwrap().to_owned())
|
Ok(snapshot.relative_path().to_str().unwrap().to_owned())
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -986,7 +990,15 @@ async fn create_backup(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let backup_time = Utc.timestamp(backup_time_opt.unwrap_or_else(|| Utc::now().timestamp()), 0);
|
let backup_time = match backup_time_opt {
|
||||||
|
Some(timestamp) => {
|
||||||
|
match Utc.timestamp_opt(timestamp, 0) {
|
||||||
|
LocalResult::Single(time) => time,
|
||||||
|
_ => bail!("Invalid backup-time parameter: {}", timestamp),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
let client = connect(repo.host(), repo.user())?;
|
let client = connect(repo.host(), repo.user())?;
|
||||||
record_repository(&repo);
|
record_repository(&repo);
|
||||||
@ -1035,7 +1047,7 @@ async fn create_backup(
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let snapshot = BackupDir::new(backup_type, backup_id, backup_time.timestamp());
|
let snapshot = BackupDir::new(backup_type, backup_id, backup_time.timestamp())?;
|
||||||
let mut manifest = BackupManifest::new(snapshot);
|
let mut manifest = BackupManifest::new(snapshot);
|
||||||
|
|
||||||
let mut catalog = None;
|
let mut catalog = None;
|
||||||
@ -1560,7 +1572,7 @@ async fn prune_async(mut param: Value) -> Result<Value, Error> {
|
|||||||
|
|
||||||
let render_snapshot_path = |_v: &Value, record: &Value| -> Result<String, Error> {
|
let render_snapshot_path = |_v: &Value, record: &Value| -> Result<String, Error> {
|
||||||
let item: PruneListItem = serde_json::from_value(record.to_owned())?;
|
let item: PruneListItem = serde_json::from_value(record.to_owned())?;
|
||||||
let snapshot = BackupDir::new(item.backup_type, item.backup_id, item.backup_time);
|
let snapshot = BackupDir::new(item.backup_type, item.backup_id, item.backup_time)?;
|
||||||
Ok(snapshot.relative_path().to_str().unwrap().to_owned())
|
Ok(snapshot.relative_path().to_str().unwrap().to_owned())
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1752,8 +1764,9 @@ async fn complete_backup_snapshot_do(param: &HashMap<String, String>) -> Vec<Str
|
|||||||
if let (Some(backup_id), Some(backup_type), Some(backup_time)) =
|
if let (Some(backup_id), Some(backup_type), Some(backup_time)) =
|
||||||
(item["backup-id"].as_str(), item["backup-type"].as_str(), item["backup-time"].as_i64())
|
(item["backup-id"].as_str(), item["backup-type"].as_str(), item["backup-time"].as_i64())
|
||||||
{
|
{
|
||||||
let snapshot = BackupDir::new(backup_type, backup_id, backup_time);
|
if let Ok(snapshot) = BackupDir::new(backup_type, backup_id, backup_time) {
|
||||||
result.push(snapshot.relative_path().to_str().unwrap().to_owned());
|
result.push(snapshot.relative_path().to_str().unwrap().to_owned());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use anyhow::{Error};
|
use anyhow::{Error};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use chrono::{TimeZone, Utc};
|
use chrono::Utc;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use proxmox::api::{ApiMethod, RpcEnvironment};
|
use proxmox::api::{ApiMethod, RpcEnvironment};
|
||||||
@ -82,7 +82,7 @@ struct BenchmarkResult {
|
|||||||
static BENCHMARK_RESULT_2020_TOP: BenchmarkResult = BenchmarkResult {
|
static BENCHMARK_RESULT_2020_TOP: BenchmarkResult = BenchmarkResult {
|
||||||
tls: Speed {
|
tls: Speed {
|
||||||
speed: None,
|
speed: None,
|
||||||
top: 1_000_000.0 * 590.0, // TLS to localhost, AMD Ryzen 7 2700X
|
top: 1_000_000.0 * 690.0, // TLS to localhost, AMD Ryzen 7 2700X
|
||||||
},
|
},
|
||||||
sha256: Speed {
|
sha256: Speed {
|
||||||
speed: None,
|
speed: None,
|
||||||
@ -212,7 +212,7 @@ async fn test_upload_speed(
|
|||||||
verbose: bool,
|
verbose: bool,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
|
|
||||||
let backup_time = Utc.timestamp(Utc::now().timestamp(), 0);
|
let backup_time = Utc::now();
|
||||||
|
|
||||||
let client = connect(repo.host(), repo.user())?;
|
let client = connect(repo.host(), repo.user())?;
|
||||||
record_repository(&repo);
|
record_repository(&repo);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::{bail, format_err, Error};
|
use anyhow::{bail, format_err, Error};
|
||||||
use chrono::{Local, TimeZone};
|
use chrono::Local;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use proxmox::api::api;
|
use proxmox::api::api;
|
||||||
@ -112,7 +112,7 @@ fn create(kdf: Option<Kdf>, path: Option<String>) -> Result<(), Error> {
|
|||||||
|
|
||||||
match kdf {
|
match kdf {
|
||||||
Kdf::None => {
|
Kdf::None => {
|
||||||
let created = Local.timestamp(Local::now().timestamp(), 0);
|
let created = Local::now();
|
||||||
|
|
||||||
store_key_config(
|
store_key_config(
|
||||||
&path,
|
&path,
|
||||||
@ -180,7 +180,7 @@ fn change_passphrase(kdf: Option<Kdf>, path: Option<String>) -> Result<(), Error
|
|||||||
|
|
||||||
match kdf {
|
match kdf {
|
||||||
Kdf::None => {
|
Kdf::None => {
|
||||||
let modified = Local.timestamp(Local::now().timestamp(), 0);
|
let modified = Local::now();
|
||||||
|
|
||||||
store_key_config(
|
store_key_config(
|
||||||
&path,
|
&path,
|
||||||
|
@ -347,7 +347,7 @@ pub async fn pull_group(
|
|||||||
let mut remote_snapshots = std::collections::HashSet::new();
|
let mut remote_snapshots = std::collections::HashSet::new();
|
||||||
|
|
||||||
for item in list {
|
for item in list {
|
||||||
let snapshot = BackupDir::new(item.backup_type, item.backup_id, item.backup_time);
|
let snapshot = BackupDir::new(item.backup_type, item.backup_id, item.backup_time)?;
|
||||||
|
|
||||||
// in-progress backups can't be synced
|
// in-progress backups can't be synced
|
||||||
if let None = item.size {
|
if let None = item.size {
|
||||||
|
@ -8,7 +8,7 @@ use std::path::Path;
|
|||||||
use anyhow::{bail, format_err, Error};
|
use anyhow::{bail, format_err, Error};
|
||||||
use nix::sys::stat::Mode;
|
use nix::sys::stat::Mode;
|
||||||
|
|
||||||
use pxar::{mode, Entry, EntryKind, Metadata};
|
use pxar::{mode, Entry, EntryKind, Metadata, format::StatxTimestamp};
|
||||||
|
|
||||||
/// Get the file permissions as `nix::Mode`
|
/// Get the file permissions as `nix::Mode`
|
||||||
pub fn perms_from_metadata(meta: &Metadata) -> Result<Mode, Error> {
|
pub fn perms_from_metadata(meta: &Metadata) -> Result<Mode, Error> {
|
||||||
@ -114,13 +114,19 @@ fn mode_string(entry: &Entry) -> String {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format_single_line_entry(entry: &Entry) -> String {
|
fn format_mtime(mtime: &StatxTimestamp) -> String {
|
||||||
use chrono::offset::TimeZone;
|
use chrono::offset::TimeZone;
|
||||||
|
|
||||||
|
match chrono::Local.timestamp_opt(mtime.secs, mtime.nanos) {
|
||||||
|
chrono::LocalResult::Single(mtime) => mtime.format("%Y-%m-%d %H:%M:%S").to_string(),
|
||||||
|
_ => format!("{}.{}", mtime.secs, mtime.nanos),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_single_line_entry(entry: &Entry) -> String {
|
||||||
let mode_string = mode_string(entry);
|
let mode_string = mode_string(entry);
|
||||||
|
|
||||||
let meta = entry.metadata();
|
let meta = entry.metadata();
|
||||||
let mtime = chrono::Local.timestamp(meta.stat.mtime.secs, meta.stat.mtime.nanos);
|
|
||||||
|
|
||||||
let (size, link) = match entry.kind() {
|
let (size, link) = match entry.kind() {
|
||||||
EntryKind::File { size, .. } => (format!("{}", *size), String::new()),
|
EntryKind::File { size, .. } => (format!("{}", *size), String::new()),
|
||||||
@ -134,7 +140,7 @@ pub fn format_single_line_entry(entry: &Entry) -> String {
|
|||||||
"{} {:<13} {} {:>8} {:?}{}",
|
"{} {:<13} {} {:>8} {:?}{}",
|
||||||
mode_string,
|
mode_string,
|
||||||
format!("{}/{}", meta.stat.uid, meta.stat.gid),
|
format!("{}/{}", meta.stat.uid, meta.stat.gid),
|
||||||
mtime.format("%Y-%m-%d %H:%M:%S"),
|
format_mtime(&meta.stat.mtime),
|
||||||
size,
|
size,
|
||||||
entry.path(),
|
entry.path(),
|
||||||
link,
|
link,
|
||||||
@ -142,12 +148,9 @@ pub fn format_single_line_entry(entry: &Entry) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn format_multi_line_entry(entry: &Entry) -> String {
|
pub fn format_multi_line_entry(entry: &Entry) -> String {
|
||||||
use chrono::offset::TimeZone;
|
|
||||||
|
|
||||||
let mode_string = mode_string(entry);
|
let mode_string = mode_string(entry);
|
||||||
|
|
||||||
let meta = entry.metadata();
|
let meta = entry.metadata();
|
||||||
let mtime = chrono::Local.timestamp(meta.stat.mtime.secs, meta.stat.mtime.nanos);
|
|
||||||
|
|
||||||
let (size, link, type_name) = match entry.kind() {
|
let (size, link, type_name) = match entry.kind() {
|
||||||
EntryKind::File { size, .. } => (format!("{}", *size), String::new(), "file"),
|
EntryKind::File { size, .. } => (format!("{}", *size), String::new(), "file"),
|
||||||
@ -196,6 +199,6 @@ pub fn format_multi_line_entry(entry: &Entry) -> String {
|
|||||||
mode_string,
|
mode_string,
|
||||||
meta.stat.uid,
|
meta.stat.uid,
|
||||||
meta.stat.gid,
|
meta.stat.gid,
|
||||||
mtime.format("%Y-%m-%d %H:%M:%S"),
|
format_mtime(&meta.stat.mtime),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use anyhow::{Error};
|
use anyhow::{Error};
|
||||||
use chrono::{TimeZone, Local};
|
use chrono::Local;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
/// Log messages with timestamps into files
|
/// Log messages with timestamps into files
|
||||||
@ -56,7 +56,7 @@ impl FileLogger {
|
|||||||
stdout.write_all(b"\n").unwrap();
|
stdout.write_all(b"\n").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let line = format!("{}: {}\n", Local.timestamp(Local::now().timestamp(), 0).to_rfc3339(), msg);
|
let line = format!("{}: {}\n", Local::now().to_rfc3339(), msg);
|
||||||
self.file.write_all(line.as_bytes()).unwrap();
|
self.file.write_all(line.as_bytes()).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use anyhow::{Error};
|
use anyhow::{Error};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use chrono::{Local, TimeZone};
|
use chrono::{Local, TimeZone, LocalResult};
|
||||||
|
|
||||||
pub fn strip_server_file_expenstion(name: &str) -> String {
|
pub fn strip_server_file_expenstion(name: &str) -> String {
|
||||||
|
|
||||||
@ -25,8 +25,11 @@ pub fn render_epoch(value: &Value, _record: &Value) -> Result<String, Error> {
|
|||||||
if value.is_null() { return Ok(String::new()); }
|
if value.is_null() { return Ok(String::new()); }
|
||||||
let text = match value.as_i64() {
|
let text = match value.as_i64() {
|
||||||
Some(epoch) => {
|
Some(epoch) => {
|
||||||
Local.timestamp(epoch, 0).format("%c").to_string()
|
match Local.timestamp_opt(epoch, 0) {
|
||||||
}
|
LocalResult::Single(epoch) => epoch.format("%c").to_string(),
|
||||||
|
_ => epoch.to_string(),
|
||||||
|
}
|
||||||
|
},
|
||||||
None => {
|
None => {
|
||||||
value.to_string()
|
value.to_string()
|
||||||
}
|
}
|
||||||
|
@ -161,10 +161,17 @@ fn parse_date_time_comp(max: usize) -> impl Fn(&str) -> IResult<&str, DateTimeVa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_date_time_comp_list(max: usize) -> impl Fn(&str) -> IResult<&str, Vec<DateTimeValue>> {
|
fn parse_date_time_comp_list(start: u32, max: usize) -> impl Fn(&str) -> IResult<&str, Vec<DateTimeValue>> {
|
||||||
move |i: &str| {
|
move |i: &str| {
|
||||||
if i.starts_with("*") {
|
if i.starts_with("*") {
|
||||||
return Ok((&i[1..], Vec::new()));
|
let i = &i[1..];
|
||||||
|
if i.starts_with("/") {
|
||||||
|
let (n, repeat) = parse_time_comp(max)(&i[1..])?;
|
||||||
|
if repeat > 0 {
|
||||||
|
return Ok((n, vec![DateTimeValue::Repeated(start, repeat)]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok((i, Vec::new()));
|
||||||
}
|
}
|
||||||
|
|
||||||
separated_nonempty_list(tag(","), parse_date_time_comp(max))(i)
|
separated_nonempty_list(tag(","), parse_date_time_comp(max))(i)
|
||||||
@ -174,9 +181,9 @@ fn parse_date_time_comp_list(max: usize) -> impl Fn(&str) -> IResult<&str, Vec<D
|
|||||||
fn parse_time_spec(i: &str) -> IResult<&str, (Vec<DateTimeValue>, Vec<DateTimeValue>, Vec<DateTimeValue>)> {
|
fn parse_time_spec(i: &str) -> IResult<&str, (Vec<DateTimeValue>, Vec<DateTimeValue>, Vec<DateTimeValue>)> {
|
||||||
|
|
||||||
let (i, (hour, minute, opt_second)) = tuple((
|
let (i, (hour, minute, opt_second)) = tuple((
|
||||||
parse_date_time_comp_list(24),
|
parse_date_time_comp_list(0, 24),
|
||||||
preceded(tag(":"), parse_date_time_comp_list(60)),
|
preceded(tag(":"), parse_date_time_comp_list(0, 60)),
|
||||||
opt(preceded(tag(":"), parse_date_time_comp_list(60))),
|
opt(preceded(tag(":"), parse_date_time_comp_list(0, 60))),
|
||||||
))(i)?;
|
))(i)?;
|
||||||
|
|
||||||
if let Some(second) = opt_second {
|
if let Some(second) = opt_second {
|
||||||
@ -190,14 +197,14 @@ fn parse_date_spec(i: &str) -> IResult<&str, (Vec<DateTimeValue>, Vec<DateTimeVa
|
|||||||
|
|
||||||
// TODO: implement ~ for days (man systemd.time)
|
// TODO: implement ~ for days (man systemd.time)
|
||||||
if let Ok((i, (year, month, day))) = tuple((
|
if let Ok((i, (year, month, day))) = tuple((
|
||||||
parse_date_time_comp_list(2200), // the upper limit for systemd, stay compatible
|
parse_date_time_comp_list(0, 2200), // the upper limit for systemd, stay compatible
|
||||||
preceded(tag("-"), parse_date_time_comp_list(13)),
|
preceded(tag("-"), parse_date_time_comp_list(1, 13)),
|
||||||
preceded(tag("-"), parse_date_time_comp_list(32)),
|
preceded(tag("-"), parse_date_time_comp_list(1, 32)),
|
||||||
))(i) {
|
))(i) {
|
||||||
Ok((i, (year, month, day)))
|
Ok((i, (year, month, day)))
|
||||||
} else if let Ok((i, (month, day))) = tuple((
|
} else if let Ok((i, (month, day))) = tuple((
|
||||||
parse_date_time_comp_list(13),
|
parse_date_time_comp_list(1, 13),
|
||||||
preceded(tag("-"), parse_date_time_comp_list(32)),
|
preceded(tag("-"), parse_date_time_comp_list(1, 32)),
|
||||||
))(i) {
|
))(i) {
|
||||||
Ok((i, (Vec::new(), month, day)))
|
Ok((i, (Vec::new(), month, day)))
|
||||||
} else {
|
} else {
|
||||||
|
@ -4,9 +4,9 @@ Ext.define('PBS.data.CalendarEventExamples', {
|
|||||||
|
|
||||||
field: ['value', 'text'],
|
field: ['value', 'text'],
|
||||||
data: [
|
data: [
|
||||||
{ value: '*/30', text: Ext.String.format(gettext("Every {0} minutes"), 30) },
|
{ value: '*:0/30', text: Ext.String.format(gettext("Every {0} minutes"), 30) },
|
||||||
{ value: 'hourly', text: gettext("Every hour") },
|
{ value: 'hourly', text: gettext("Every hour") },
|
||||||
{ value: '*/2:00', text: gettext("Every two hours") },
|
{ value: '0/2:00', text: gettext("Every two hours") },
|
||||||
{ value: '2,22:30', text: gettext("Every day") + " 02:30, 22:30" },
|
{ value: '2,22:30', text: gettext("Every day") + " 02:30, 22:30" },
|
||||||
{ value: 'daily', text: gettext("Every day") + " 00:00" },
|
{ value: 'daily', text: gettext("Every day") + " 00:00" },
|
||||||
{ value: 'mon..fri', text: gettext("Monday to Friday") + " 00:00" },
|
{ value: 'mon..fri', text: gettext("Monday to Friday") + " 00:00" },
|
||||||
|
@ -5,6 +5,8 @@ Ext.define('PBS.window.SyncJobEdit', {
|
|||||||
|
|
||||||
userid: undefined,
|
userid: undefined,
|
||||||
|
|
||||||
|
onlineHelp: 'syncjobs',
|
||||||
|
|
||||||
isAdd: true,
|
isAdd: true,
|
||||||
|
|
||||||
subject: gettext('SyncJob'),
|
subject: gettext('SyncJob'),
|
||||||
|
Reference in New Issue
Block a user