1
0
Fork 0
Commit graph

82 commits

Author SHA1 Message Date
Julian Smith
c872af41bf Allow AI carrier to be always tied exactly to MP carrier.
Also fixed uneven MP carrier motion.

If /ai/models/carrier[]/ai-latch is true (e.g. by MPCarrier.nas), we
set /position/* and /orientation/* in C++ every frame instead of in
nasal. For external multiplayer carriers the values are copied from
/ai/models/multiplayer[]/*, so the AI carrier follows the corresponding MP
carrier exactly. For this to be useful, multiplayer motion needs be smooth,
e.g. with /sim/time/simple-time/enabled=true.

scripts/python/recordreplay.py
    Added --carrier test - checks that multiplayer carrier moves with even
    speed.

src/AIModel/AIBase.hxx
    Added speed_fps, to be kept up to date and tied to velocities/uBody-fps
    when we are a carrier. Previously this was set from Nasal which doesn't
    do what is required when all updates happen in C++.

src/AIModel/AICarrier.cxx
src/AIModel/AICarrier.hxx
    If is-user-craft is true, we directly update /position/* every frame.

    If ai-latch is true, we don't call TurnToLaunch(), TurnToRecover(),
    ReturnToBox(), TurnToBase() etc, because our position and orientation is
    determined only by multiplayer packets.

src/AIModel/AIMultiplayer.cxx
src/AIModel/AIMultiplayer.hxx
    If an mp craft is a carrier, MP packets define velocities/speed-kts but
    set ecLinearVel to all-zeros. So we now copy across to ensure that the
    extrapolation algorithm has a velocity to work with. Previously the zero
    velocity caused very uneven motion.

    If ai-latch is true we set AI craft's position+velocities directly from
    the equivalent (extrapolated or interpolated) MP position, every frame. We
    also set AI craft's orientation. And we copy MP's uBody-fps to AI's
    velocities/speed-kts which ensures that friction works between carrier deck
    and aircraft undercarriage.

    Added logging of raw speeds implied by multiplayer packets, activated by
    /sim/log-multiplayer-callsign; used by scripts/python/recordreplay.py's
    --carrier test.

src/AIModel/AIShip.cxx
    Tie velocities/uBody-fps to new speed_fps member and set speed_fps in
    update() along with members that are tied to properties.

    Replaced code that calculated new position using heading and speed:

        Previously the new position after dt was calculated using
        ft_per_deg_lat, ft_per_deg_lon, speed_north_deg_sec and
        speed_east_deg_sec. But this was moving slightly faster than the
        specified speed.

        This was leading to incremental errors when a different Flightgear
        instance extrapolated the multiplayer position from the information in
        multiplayer packets, because the specified velocity was too small, so
        we jumped forwards when extrapolation moved to a new packet.

        The fix is to use a Quaternion-based calculation to calculate movement
        in the direction specified by (heading, pitch, roll), as done by other
        code such as the view code.

src/Main/fg_init.cxx
    Moved FGAIManager to just before FGMultiplayMgr so we send latest info in
    mp packets.

src/MultiPlayer/multiplaymgr.cxx
    Fixed minimum transmit rate calculation - if transmit rate is less than 1,
    default to 1, not 10.

src/Network/props.cxx
    Use more precision when sending double-precision values e.g. to telnet
    client. Otherwise for example UTC times don't have sufficient resolution.

src/Viewer/viewmgr.cxx
    Generate internal logs of multiplayer position and speed (after
    interpolation/extrapolation) if /sim/log-multiplayer-callsign is set. Used
    by scripts/python/recordreplay.py's --carrier test.
2021-06-25 11:08:12 +01:00
Julian Smith
7753ce229d Fixed uneven replay of user vs multiplayer aircraft.
FGReplay was using time t, AIMultiplayer was using t+dt. The fix is to make
FGReplay also use t+dt.

scripts/python/recordreplay.py
    Make --test-motion-mp test for this problem - we check that user and mp
    aircraft are a constant distance apart when replaying.

src/AIModel/AIMultiplayer.cxx
    Create properties showing distance moved since start by user and mp
    aircraft, which can then be tested by recordreplay.py.

src/Aircraft/replay.cxx
    Increment current_time by dt before calling replay(current_time), to
    ensure that replay() sees the time as is later used by AIMultiplayer (via
    /sim/replay/time).
2021-04-18 12:12:50 +01:00
Julian Smith
99bcc3ba24 scripts/python/FlightGear.py: ls() now returns typed values. 2021-04-18 12:12:50 +01:00
Julian Smith
34955d820b scripts/python/recordreplay.py: use simple-time system.
test_motion() now sets /sim/time/simple-time/enabled=true.

Also show any description items - these are generate by related changes to
src/AIModel/AIMultiplayer.cxx.
2021-04-15 17:23:58 +01:00
Julian Smith
6aa0e4a5cb scripts/python/FlightGear.py: fixed handling of large outputs.
Telnet.expect() can return short result, so _getresp() needs to call it in a
loop.
2021-04-15 17:23:58 +01:00
Julian Smith
f08076c391 scripts/python/recordreplay.py: fixed when fgfs not run via wrapper script. 2021-04-11 10:06:13 +01:00
Julian Smith
29dc9ea93a scripts/python/recordreplay.py: fixed use when fgfs is run via wrapper script.
Various tweaks to motion tests.
2021-04-10 10:39:18 +01:00
Julian Smith
9d8a10f8b7 scripts/python/recordreplay.py: cope if --tape-dir is already configured.
We now always specify our own --tape-dir.

Documented --test-motion-mp option.

Also patched up cleanup of recordings.
2021-04-04 11:59:28 +01:00
Julian Smith
3fa13b7762 scripts/python/recordreplay.py: also analyse final mp speed.
This is in addition to mp speed in mp packets. Looks like this final mp speed
is very uneven.

Have changed the test slightly to move both UFOs at the same speed so replaying
e.g. with Helicopter View shows them incorrectly moving relative to each other.
2021-03-28 17:28:53 +01:00
Julian Smith
5de5419e25 scripts/python/recordreplay.py: added test of multiplayer information.
New option: --test-motion-mp checks raw speed of multiplayer aircraft while
replaying.
2021-03-27 23:04:24 +00:00
Julian Smith
5c76f41743 Added support for analysing raw speed values when replaying.
Setting sim/replay/log-raw-speed logs raw speed to
/sim/replay/log-raw-speed-values/value[].

'scripts/python/recordreplay.py --test-motion' uses this to test for a bug
where the user aircraft simtime and signals information don't quite match in
continuous recordings.
2021-03-27 22:37:54 +00:00
Julian Smith
797f02f9dd Allow listing of property children via telnet.
Adds new ls2 command with machine-readable output.
2021-03-27 22:37:47 +00:00
Julian Smith
2d35a52f8a scripts/python/recordreplay.py: minor fix.
Don't rely on top-level <fgfs> variable.
2021-03-07 21:57:50 +00:00
Julian Smith
b74d96ca81 scripts/python/recordreplay.py: set sim/replay/record-signals to true in case it defaults to false.
Otherwise recordings end up containing very few frames which breaks some of our
tests.
2021-03-06 08:52:20 +00:00
Julian Smith
9bd01e4cc1 scripts/python/recordreplay.py: use subprocess instead of os.fork() etc.
This might allow use on non-unix systems, or at least be a step towards that.
2021-03-06 08:52:20 +00:00
Julian Smith
cb1cf90490 scripts/python/FlightGear.py: Fix setting property to True.
On flightgear side, the string 'True' is not treated as bool true, so if a
property is set to python True, we need to send the string 'true' instead.
2021-03-06 08:52:20 +00:00
Julian Smith
31ec727872 Added record/replay of extra properties, with specific support for window size/position and view settings.
Recording of extra properties is only supported in Continuous recordings.

Modified Continuous recording format to allow future forwards
compatibility. See docs-mini/README-recordings.md for details. This breaks
compatibility with previously-generated Continuous recordings, but this only
affects next.

To reduce overhead we record all extra property values in the first frame and
then later frames contain only extra property changes. When replaying, if the
user jumps backwards we replay all extra property changes since the start of
the recording. Similarly if the user jumps forwards, we replay any intervening
extra property changes.

Recording extra properties:
    This is enabled by:
        /sim/replay/record-extra-properties

    The extra properties that are recorded are identified by the property
    paths in the values of /sim/replay/record-extra-properties-paths/path[]
    properties. We record the entire tree for each specified path.

Recording of main window position size:
    We have specific support for record and replay of main window position/size.

    This is enabled by:
        /sim/replay/record-main-window

Recording of main window view:
    We have specific support for recording the view type and direction/zoom
    settings.

    This is enabled by:
        /sim/replay/record-main-view

    We record the /sim/current-view/ property tree, excluding some subtrees
    that continuously vary but are not required for replaying of the view.

When replaying, we allow separate control of what extra property changes are
replayed, with:

    /sim/replay/replay-extra-property-changes
    /sim/replay/replay-extra-property-removal
    /sim/replay/replay-main-window-position
    /sim/replay/replay-main-window-size
    /sim/replay/replay-main-view

We work around some problems caused by the use of tied properties when
replaying changes to view-number.

Window position issue:

    When replaying window position and size changes, things get a little tricky
    because osgViewer::GraphicsWindow::setWindowRectangle() takes a position
    for the entire window, but osgGA::GUIEventAdapter::RESIZE events contain
    the position of the interior of the window; for example the y values will
    differ by the height of the window's titlebar. This can cause windows to
    move progressively further down each time they are positioned or resized.

    There doesn't seem to be a way of finding the size of a window's
    furniture directly. So instead this commit adds a new method
    osgGA::GUIEventAdapter::setWindowRectangleInteriorWithCorrection() which
    wraps osgViewer::GraphicsWindow::setWindowRectangle(). We listen for the
    following osgGA::GUIEventAdapter::RESIZE event and calculate corrections
    for x and y position that are used for subsequent calls.

docs-mini/README-recordings.md:
    Updated to document new Continuous format.

scripts/python/recordreplay.py:
    New script to test various aspects of record/replay.

Other:

    We now create convenience softlink to most recent continuous recording, using
    SGPath::makeLink(). Note that SGPath::makeLink() currently does nothing on
    Windows.

    Changed format of Continuous recordings to contain a single property tree
    in header. This is much simpler than having separate Config and Meta trees.
2021-02-13 11:38:52 +00:00
Scott Giese
0058ef27c5 TerraSync: Rate Limiter
Sourceforge seems to have an agressive API rate limiter set at approximately 50 requests/minute.
2021-01-31 21:58:41 -06:00
Torsten Dreyer
7621569834 terrasync.py: add option to use basic authentication 2021-01-31 14:25:06 +01:00
Florent Rougon
e342a0f41e terrasync.py: add support for the HTTPS protocol
- Instantiate an HTTPSConnection object when the URL scheme is 'https'.

- Clarify and simplify the initialization of HTTPSocketRequest and
  HTTPDownloadRequest:
    + clarify initialization of their 'callback' attribute (it's the
      method of the same name; make it clear that the base class
      constructor, namely HTTPGetCallback.__init__(), doesn't modify the
      'callback' attribute when an object of class HTTPSocketRequest or
      HTTPDownloadRequest is initialized);
    + HTTPDownloadRequest doesn't need access to the TerraSync object
      -> remove the corresponding instance attribute and constructor
         argument.

- Don't use super() when initializing HTTPDownloadRequest objects
  (see [1]).

[1] https://fuhm.net/super-harmful/
2020-12-15 15:40:40 +01:00
Florent Rougon
c5e45f2b49 terrasync.py: two improvements
- Refuse to recursively delete a directory that does not contain a
  .dirindex file. This will protect users against data loss in case they
  inadvertently use the --remove-orphan option with the wrong target
  directory.

- Correctly handle the case where we have a file on disk that is now
  listed as a directory on the server: remove the file if we are in
  'sync' mode, so that the directory can be created and sync'ed from the
  server.
2020-10-04 14:02:13 +02:00
Scott Giese
3f2ee2de04 Use python3 default implementation 2020-10-03 10:28:46 -05:00
Florent Rougon
8009f46a51 terrasync.py: improve code readability 2020-10-03 14:55:25 +02:00
Florent Rougon
692ab6835f terrasync.py: more thorough checking of .dirindex contents
- only accept ASCII-encoded .dirindex files (this is guaranteed to work
  fine "everywhere");

- reject .dirindex files with a 'path' entry that contains a backslash
  or starts with a slash;

- reject .dirindex files with a 'path' entry that contains a '..'
  component;

- reject .dirindex files with an 'f', 'd' or 't' entry whose name field
  contains a slash or a backslash;

- reject .dirindex files with an 'f', 'd' or 't' entry whose name field
  is '..';

- add comment lines (starting with '#') in the sample good .dirindex
  file used by unit tests.
2020-10-03 14:18:29 +02:00
Florent Rougon
2a991c9874 terrasync.py: test_virtual_path.py can't be run directly
Remove the 'if __name__ == "__main__": unittest.main()'. Indeed, the
module can't be run this way due to its imports. Tests from this module
can be run with:

  cd scripts/python/TerraSync
  python3 -m unittest tests.test_virtual_path
2020-10-02 16:38:08 +02:00
Florent Rougon
13f943b4a1 terrasync.py: rename DirIndex attributes and remove accessors
In Python, common usage is not to define accessors, but to directly use
class or instance attributes (especially when the associated data is
constant after instance creation). If it later happens that a given
attribute needs getter or setter logic, this can always be done via the
@property decorator, and doesn't affect calling code at all. See for
instance:

  https://docs.python.org/3/library/functions.html#property
  https://mail.python.org/pipermail/tutor/2012-December/thread.html#92990

Apply this to the DirIndex class and rename the following attributes for
better readability: f -> files, d -> directories, t -> tarballs.
2020-10-02 16:38:08 +02:00
Florent Rougon
477d9f7a9a terrasync.py: move the DirIndex class to its own module and add unit tests
The tests can be run from directory 'scripts/python/TerraSync' using:

  python3 -m unittest tests.test_dirindex

(or just 'python3 -m unittest' to run all tests pertaining to
terrasync.py).
2020-10-02 16:38:08 +02:00
Scott Giese
431844138b python compatibility: make __ne__ explicit.
python3 has a default implementation for __ne__ when __eq__ is defined.  The opposite is not true -- having only __ne__ does not have a default __eq__ implementation.
Also note that there are cases when eq/ne will both be True or both False, therefore, developers are encouraged to explicitly define these methods in pairs.
2020-10-01 23:06:37 -05:00
Scott Giese
22e9d0e2f1 python: use a with-statement to manage file close 2020-10-01 22:51:31 -05:00
Florent Rougon
7714abd56e terrasync.py: fix a DeprecationWarning
Using or importing the ABCs from 'collections' instead of from
'collections.abc' is deprecated since Python 3.3, and in 3.9 it will
stop working.
2020-10-01 19:44:19 +02:00
Scott Giese
df67cc2bd9 Python: best practices
Check eq/ne
Anticipate file issues
Ensure file closure
2020-08-29 12:07:15 -05:00
Thomas LESNE
31f434d3ad Migrate FGFSDemo.py, FlightGear.py, demo.py and nasal_api_doc.py to Python 3
Also apply minor changes to FGFSDemo.py and demo.py: add python3
shebang, make the scripts executable, improve an error message.
This closes ticket #224:
<https://sourceforge.net/p/flightgear/flightgear/merge-requests/224/>.
2020-08-05 14:01:57 +02:00
Thomas LESNE
8e3274a7c2 FGFSDemo.py: update properties to match the ones available in 2020.x
New properties available at least with the c172p.
2020-08-05 12:45:59 +02:00
Stuart Buchanan
9553577397 Add support for tarballs in terrasync 2020-02-16 20:27:44 +00:00
Scott Giese
b0e157dd88 Bug fix to set a valid default value for the right edge.
Courtesy of report by Brendan Black.
2018-10-04 18:59:15 -05:00
Scott Giese
e093ac19bc Bug Fix: #2013.
TerraSync not download all tiles specified in some cases.
Contribution by Peter Duda.
2018-09-27 23:46:16 -05:00
Florent Rougon
c7f6c55423 terrasync.py: user-friendly error message when no 'path' found in .dirindex
During the recent SourceForge migration, the TerraSync server hosted
there used to send .dirindex files with the following contents:

  Project web is currently offline pending the final migration of its
  data to our new datacenter.

Make sure terrasync.py aborts with a user-understandable error in such a
case.
2018-02-18 20:22:58 +01:00
Florent Rougon
5cca99bbae virtual_path.py: add more functions and properties to VirtualPath
Add more functions and properties to VirtualPath that directly
correspond to functions and properties of pathlib.PurePath, except that
types are adapted of course, and that for API consistency, VirtualPath
methods use mixedCaseStyle whereas those of pathlib.PurePath use
underscore_style.
2018-02-18 19:04:58 +01:00
Florent Rougon
dd4fc36a9d terrasync.py: make assert statements more useful
When the second argument of an assert statement is a string, using
repr() is more helpful than relying on the default representation.
2018-02-18 19:04:58 +01:00
Florent Rougon
5aae639a0d terrasync.py: print full URL in exception messages, pass full URL to callback
Due to some misleading 'url' variable name, network error messages used
to contain things such as '/scenery/Airports/N/E/4/.dirindex' (i.e., the
path on the server) instead of the full URL. For the same reason, the
callback function of HTTPGetCallback objects was passed this
path-on-server instead of the URL. This should all be fixed now.
2018-02-08 00:10:18 +01:00
Florent Rougon
1ae1ecc6c2 terrasync.py: accept paths using backslash separators for --only-subdir
We don't lose anything by accepting this, because using backslashes in
file or dir names is out of question, as Windows users can't see them.
2018-02-07 23:31:38 +01:00
Florent Rougon
cb6b267430 terrasync.py: fix a small formatting issue with --report
Remove unneeded blank lines between listed files or dirs when using
--report.
2018-02-07 11:38:41 +01:00
Florent Rougon
c30298ffce terrasync.py: add option --only-subdir
Option --only-subdir allows one to restrict terrasync.py processing[1]
to a chosen subdirectory of the TerraSync repository. Example:

  terrasync.py --target=/your/TerraSync/repo --only-subdir="Airports/L/F/P"

[1] This works in both 'check' and 'sync' modes.
2018-02-07 11:38:41 +01:00
Florent Rougon
c0e1f29a75 terrasync.py: add and use a VirtualPath class; also add MutableVirtualPath
Add classes VirtualPath and MutableVirtualPath (the latter derived from
the former) to manipulate slash-separated paths where the root '/'
represents the TerraScenery root. This makes it clear what a function
expects when you see that one of its arguments is a VirtualPath
instance: you don't have to ask yourself whether it can start or end
with a slash, how to interpret it, etc. Operating on these paths is also
easy[1], be it to assemble URLs in order to retrieve files or to join
their relative part with a local directory path in order to obtain a
real (deeper) local path.

VirtualPath and MutableVirtualPath are essentially the same; the former
is hashable and therefore has to be immutable, whereas the latter can be
modified in-place with the /= operator (used to append path components),
and therefore can't be hashable. As a consequence, MutableVirtualPath
instances can't be used as dictionary keys, elements of a set or
frozenset, etc.

VirtualPath and MutableVirtualPath use the pathlib.PurePath API where
applicable (part of this API has been implemented in
[Mutable]VirtualPath; more can be added, of course). These classes have
no assumptions related to TerraSync and thus should be fit for use in
other projects.

To convert a [Mutable]VirtualPath instance to a string, just use str()
on it. The result is guaranteed to start with a '/' and not to end with
a '/', except for the virtual root '/'. Upon construction, the given
string is interpreted relatively to the virtual root, i.e.:

  VirtualPath("") == VirtualPath("/")
  VirtualPath("abc/def/ghi") == VirtualPath("/abc/def/ghi")
  etc.

VirtualPath and MutableVirtualPath instances sort like the respective
strings str() converts them too. The __hash__() method of VirtualPath is
based on the type and this string representation, too. Such objects can
only compare equal (using ==) if they have the same type. If you want to
compare the underlying virtual paths inside a VirtualPath and a
MutableVirtualPath, use the samePath() method of either class.

For more info, see scripts/python/TerraSync/terrasync/virtual_path.py
and unit tests in scripts/python/TerraSync/tests/test_virtual_path.py.

[1] Most useful is the / operator, which works as for SGPath:

      VirtualPath("/abc/def/ghi") == VirtualPath("/abc") / "def" / "ghi"
      VirtualPath("/abc/def/ghi") == VirtualPath("/abc") / "def/ghi"
2018-02-07 11:38:41 +01:00
Florent Rougon
e613c81b4c terrasync.py: move custom exception classes to a separate module
Add new module 'terrasync.exceptions' for our custom exception classes.
2018-02-07 11:38:41 +01:00
Florent Rougon
c72de824d2 terrasync.py: more modular code layout
- New directory scripts/python/TerraSync/terrasync.

- Move scripts/python/terrasync.py to
  scripts/python/TerraSync/terrasync/main.py (main module in the new
  structure).

- Add empty __init__.py file to scripts/python/TerraSync/terrasync/ to
  make this directory a Python package.

- Wrap the main code from previous terrasync.py in a main() function of
  the terrasync.main module. Also move command-line arguments parsing to
  a separate parseCommandLine() function.

- Add an executable script scripts/python/TerraSync/terrasync.py for end
  users, that just calls terrasync.main.main().

For end users, the only difference is that they now have to use
scripts/python/TerraSync/terrasync.py instead of
scripts/python/terrasync.py (which doesn't exist anymore, since all this
lives under the scripts/python/TerraSync directory from now on).

This structure will allow to cleanly split the code into modules and to
add unit tests.
2018-02-07 11:38:41 +01:00
Florent Rougon
eb23f8906d terrasync.py: use os.path.abspath() in TerraSync.setTarget()
Using os.path.abspath() here in TerraSync.setTarget() adds a safety
layer in case the process later calls os.chdir() or similar[1], which
would change the meaning of the "." directory. Also remove the strip()
call which I don't consider useful here, see my message at:

  https://sourceforge.net/p/flightgear/mailman/message/36208140/

[1] Not the case currently, but who knows what will happen in the
    future...
2018-02-03 11:22:56 +01:00
Florent Rougon
77513a2498 terrasync.py: update script header
In particular, terrasync.py appears *not* to require dnspython. :-)
2018-02-03 11:22:56 +01:00
Florent Rougon
8693e442d7 terrasync.py: add options --mode and --report
You may now call terrasync.py with --mode=sync or --mode=check. 'sync'
mode is the default and corresponds to terrasync.py's usual behavior.

In 'check' mode, terrasync.py never writes to disk and aborts at the
first mismatch between local and remote data. The exit status in 'check'
mode is:
  - 0 if the program terminated successfully and no mismatch was found
    between the local and remote repositories;
  - 1 in case an error was encountered;
  - 2 if there was a mismatch between local and remote data.

In 'sync' mode, the exit status is:
  - 0 if the program terminated successfully;
  - 1 in case an error was encountered.

A mismatch in 'check' mode is *not* an error, it is just one of the two
expected results. An error is a worse condition (uncaught exception,
network retrieval aborted after retrying failed, stuff like that).

Additionally, calling terrasync.py with --report causes it to print
lists of:
  - files and dirs that were missing or had mismatching hashes (this is
    okay in 'sync' mode: these things have been "fixed" in the target
    directory before the report was printed);
  - files and dirs that have been found to be orphaned (i.e., found
    under the target directory but not mentioned in the corresponding
    .dirindex file). These are the ones removed in 'sync' mode when
    --remove-orphan is passed.
2018-02-03 11:22:56 +01:00
Florent Rougon
6d323bbbdc terrasync.py: prepare the terrain for --mode and --report
- Add computeHash() utility function that can work with any file-like
  object (e.g., a connected socket).

- Rename hash_of_file() to hashForFile(), and of course implement it
  using our new computeHash().

- Add class HTTPSocketRequest derived from HTTPGetCallback. It allows
  one to process data from the network without storing it to a file (it
  uses the file-like interface provided by http.client.HTTPResponse).

  The callback returns the http.client.HTTPResponse object, which can be
  conveniently used in a 'with' statement.

- Simplify the API of TerraSync.updateDirectory(): its 'dirIndexHash'
  argument must now be a hash (a string); the None object is not allowed
  anymore (with the soon-to-come addition of --mode=check, having to
  deal with this special case in updateDirectory() would make the logic
  too difficult to follow, or we would have to really completely
  separate check-only mode from update mode, which would entail code
  duplication).

  Since TerraSync.updateDirectory() must now always have a hash to work
  with, compute the hash of the root '.dirindex' file from the server in
  TerraSync.start(), using our new HTTPSocketRequest class---which was
  written for this purpose, since that will have to work in check-only
  mode (but not only), where we don't want to write any file to disk.

- TerraSync.updateFile(): correctly handle the case where a directory
  inside the TerraSync repository is (now) a file according to the
  server: the directory must be recursively removed before the file can
  be downloaded in the place formerly occupied by the directory.

- Add stub class Report. Its methods do nothing for now, but are already
  called in a couple of appropriate places. The class will be completed
  in a future commit, of course.
2018-02-03 11:22:56 +01:00