Commit Graph

385 Commits

Author SHA1 Message Date
Dal Rupnik 100fc1e8bc fix: harden drag-and-drop against the edge-case report
Introduces DropHandler to own the drop pipeline and addresses every item
from the review:

- Folders / .app bundles / packages: DropProgressCopier.copyTree walks
  directories instead of failing with a generic 'Failed to copy'.
- File promises (Photos, Mail, browser image drags) are now accepted and
  materialized instead of silently no-opping.
- Multi-file toast race: per-file DropSession id; stale relocation
  results for a superseded file are ignored by update/finish/
  setFinalDestination.
- Partial files: copyTree removes its partial output on any error, and
  the handler drops the now-empty subdir, so the guest never sees a
  truncated file.
- Teardown race: in-flight guest relocations register with
  RelocationGate; 'tart run' drains it (<=6s) before deleting the drop
  zone / exiting.
- Path collisions: each file copies into its own dropRoot/<uuid>/ subdir.
- Reserved name: a user --dir named 'Dropped Files' is now rejected.
- Linux / no agent: relocation is skipped and the toast says 'Copied to
  the shared folder' instead of a misleading Finder destination.
- Agent-down timing: success toast holds on a fallback timer that
  outlives the 5s RPC deadline so the final destination is always shown.
- Concurrency: relocations run one-at-a-time; copy failures are
  coalesced into a single alert instead of a modal storm.
- Zero-byte/unknown size shows just the copied amount, not '0 bytes of ?'.
- Removed dead DropFolderBox + stale doc comment; fixed the
  http://-vs-https:// typo in toRemoteOrLocalURL.

Tests: DropProgressCopierTests (replace/cancel/error-cleanup/empty/
directory/size), GuestDropParseTests (stdout parser), and a reserved-name
case in DirectoryShareTests. Product and test target both compile.
2026-05-20 10:15:08 +02:00
Dal Rupnik 3bae4c62cf style: apply SwiftFormat indent to GuestDropSynthesis.swift
Pre-existing 2-space indentation drift; no semantic change. Brings the
file in line with the project's mandated SwiftFormat config.
2026-05-20 10:15:08 +02:00
Dal Rupnik 93d5708c08 refactor: split DropProgressToast into focused sub-250-line files
DropProgressToast.swift (436 lines) bundled four responsibilities. Split
verbatim into:

- DropCancellationToken.swift  - cancel token + DropCopyCancelled sentinel
- DropProgressCopier.swift     - chunked file copier
- DropProgressToast.swift      - toast state/coordination (lifecycle API)
- DropProgressToast+Panel.swift - AppKit panel construction/positioning

Each file is well under 250 lines and single-responsibility. The only
non-move change is widening the members the +Panel extension references
from private to internal; no behavior, signatures, or logic changed.
2026-05-20 10:15:08 +02:00
Dal Rupnik 9e5f65b8da feat: drop file into the Finder window under the cursor
Previously the guest-agent relocation script always asked Finder for the
*front* window and dropped there. That meant dropping on the bare Desktop
while a Downloads window happened to be frontmost put the file in
Downloads — the opposite of user intent.

Plumb the drop point (already captured as a normalized 0..1 top-left
coordinate by DropGeometry.normalize) through synthesizeGuestDrop and
GuestDropSynthesis.perform into the in-guest script. The script now
converts the point to guest-screen pixels using the desktop window's
bounds, walks `every Finder window` in front-to-back order, and returns
the folder of the first window whose bounds contain the point. If no
window does, dest_dir stays empty and the existing fallback drops the
file on ~/Desktop — which is correct for a drop on bare Desktop.

The AppleScript body is piped to `osascript -` via a single-quoted bash
variable (rather than a heredoc) so it survives Swift's multi-line
string indentation rules cleanly.
2026-05-20 10:15:08 +02:00
Dal Rupnik 0cc0969652 feat: show drop destination in toast and unblock guest agent on dev builds
After a host→guest drop completes, the toast now updates from "Done" to
"Copied to <Folder>" (e.g. "Desktop", "Documents") once the guest agent
has finished relocating the file out of the share. When the agent isn't
reachable, the toast falls back to "Copied to Shared Files" so the user
always sees a sensible destination.

Three things had to come together:

1. Toast surfaces the destination

   `GuestDropSynthesis.perform` now returns a `GuestDropOutcome` carrying
   the basename of the destination folder (the in-guest script appends
   a `tartdrop-dest=<basename>` line after `mv`). The relocation runs as
   a fire-and-forget Task off the copy queue and patches the toast via a
   new `setFinalDestination` method — which uses a shared
   `pendingFinalText` slot so a fast relocation result doesn't get
   clobbered by the delayed "Done" placeholder.

2. gRPC timeout shortened so failures fit in the toast window

   The exec-call timeout drops from 8 s to 5 s. Steady-state calls
   finish in well under 500 ms; 5 s leaves headroom for a first-run
   osascript blocked on a Finder Automation TCC prompt inside the
   guest. The baseline hide on success grows to 2 s so the destination
   update has a chance to land before the panel disappears.

3. Dev builds of tart can now host the guest agent

   `CI.version` returns `"SNAPSHOT"` for non-tagged builds, which gave
   the VM a console port named `tart-version-SNAPSHOT`. The guest agent
   parses that suffix as a semver and falls back to `unix.Kill(getppid,
   SIGTERM)` when it can't — which fails with EPERM against launchd and
   prints "operation not permitted" every 10 s forever. A new
   `CI.deviceVersion` always emits a valid semver with major ≥ 2
   (`"99.0.0"` for SNAPSHOT) and VM.swift uses it for the port name.
   `99.0.0` without a `-prerelease` suffix is required because macOS's
   BSD tty layer rejects the dotted+hyphenated form and refuses to
   expose the device under `/dev/cu.*`.
2026-05-20 10:15:08 +02:00
Dal Rupnik 2db423c404 fix: overlay drop toast inside VM window with fade-in and synced "Done"
Three related fixes to the drop progress toast:

1. Position the toast over the VM window's content (top-right, 44 pt
   below the titlebar) instead of floating outside above the window.
   This reverts the layout intent of the prior "float above" commit;
   the toast remains an NSPanel child-windowed to the VM window so it
   tracks z-order and movement.

2. Replace the broken slide-in animation with a fade-in. The previous
   animation called `panel.animator().setFrameOrigin(target)`, which is
   a silent no-op on NSWindow — the panel jumped to the off-screen-right
   start position and stayed there. Use `animator().alphaValue` instead,
   which is actually animatable on NSWindow.

3. Sync the "Done" text with the bar reaching 100%. NSProgressIndicator
   has an undocumented ~0.3 s smooth-fill animation when doubleValue
   jumps, so showing "Done" simultaneously with setting maxValue made the
   text lead the bar. Delay "Done" by 0.35 s and extend the hide delay
   to 1.05 s so "Done" still gets ~0.7 s of visibility. Also reset the
   bar in `hide` so a subsequent drop doesn't briefly flash the previous
   final state before resetting to 0.
2026-05-20 10:15:08 +02:00
Dal Rupnik 084e69b644 fix: float drop toast above the VM window, not inside it
The notification banner used to sit inside the VM window's top-right
corner, overlapping guest content. Move it outside the window: the
toast's bottom edge now sits 10 pt above the VM window's top edge,
still right-aligned. The slide-in animation is unchanged.

If the VM window is jammed against the top of the screen and there's
no room above for the toast, fall back to the old "inside top-right"
position so we never clip the menu bar.
2026-05-20 10:15:08 +02:00
Dal Rupnik 83be453200 feat: move drop toast to top-right and add cancel button
Two changes to the drop progress HUD:

  - Position: was bottom-center of the VM window, now top-right with a
    quick (~0.18 s) slide-in from the right edge — reads as a macOS
    notification banner. Subsequent files in a multi-file drop retarget
    the panel in place without replaying the animation.

  - Cancel: small ⊗ close button in the toast's top-right corner. Click
    flips a `DropCancellationToken` shared with the chunked copier,
    which polls between 1 MiB chunks and throws `DropCopyCancelled` on
    the next boundary (sub-100 ms latency on fast disks). The drop
    handler removes the half-copied destination, the toast shows
    "Cancelled", and any remaining files in a multi-file drop are
    skipped instead of starting.
2026-05-20 10:15:08 +02:00
Dal Rupnik 8c684f52e4 feat: show progress-bar toast during host→guest drop copy
`FileManager.copyItem` is opaque to the user: a 5 GB drop just freezes
the cursor for ten seconds with no visible signal that anything is
happening. Replace it with a chunked `FileHandle` copy (1 MiB chunks,
50 ms-throttled progress callbacks) and render a borderless HUD panel
anchored to the VM window bottom — filename, determinate bar, and a
"[i/N] copied / total" detail line. The panel auto-hides ~0.8 s after
the final byte so the user sees the "Done" state.

Multi-file drops reuse the same panel and increment the [i/N] counter,
since the existing copy loop already serializes files.
2026-05-20 10:15:08 +02:00
Dal Rupnik 6af39e807f feat: relocate dropped files to frontmost Finder folder, not the share
The previous guest-drop synthesis only ran `open -R` on the file's
location in the "Dropped Files" share. That just revealed it in the
share, which was the user-visible bug we were trying to fix: dropped
files appeared in `/Volumes/My Shared Files/Dropped Files/` instead
of somewhere natural.

Now the agent's exec script asks Finder for the frontmost window's
POSIX path via `osascript`, moves the file out of the share into that
folder, and then reveals it. If no Finder window is open, the path
isn't writable, or it points back at the share itself, the script
falls back to `~/Desktop`. Filename collisions get suffixed
(`foo.txt` → `foo 2.txt`, …) instead of clobbering.

osascript here runs in the agent's user-GUI session, so the first
drop triggers an Automation→Finder TCC prompt in the guest the user
approves once.

Verified end-to-end via `tart exec` (same RPC path the drop handler
uses): no Finder window → ~/Desktop, Finder on Downloads → Downloads,
Finder on Dropped Files itself → ~/Desktop fallback.
2026-05-20 10:15:08 +02:00
Dal Rupnik 474fd47b6f feat: scaffold guest-side drop synthesis (host plumbing only)
Adds the host-side hook for the forthcoming tart-guest-agent DragAndDrop RPC
that will perform a real drag-and-drop at the cursor's position inside the
guest instead of dumping files into a generic share folder.

- DropGeometry.normalize: pure helper turning a view-local drop point into
  top-left (0..1) coordinates the guest agent can map onto its screen.
- VMContainerView.performDragOperation: capture draggingLocation on the
  main thread (only valid here), compute normalized point, then dispatch
  the copy off-main as before.
- synthesizeGuestDrop: documented no-op stub called from the copy completion
  path. When the agent RPC lands, this becomes the real call site; until
  then it returns false and the existing share-folder copy is the
  user-visible result.

No behavior change for users today.
2026-05-20 10:15:08 +02:00
Dal Rupnik 2c5037ea50 refactor: harden drag-and-drop drop zone lifecycle and copy path
- Hold a FileLock on the dropzone so a concurrent tart command's
  Config.gc() can't delete it from under the running VM, and remove
  the directory explicitly on VM exit (defer doesn't fire through
  Foundation.exit).
- Extract DirectoryShare.collect() and add the drop zone as a named
  "Dropped Files" share so the share-builder logic stays simple.
- Copy dropped files on a background queue so large drops don't
  freeze the VM framebuffer (the view that handles the drop also
  renders the VM).
- Reject unnamed --dir combined with drag-and-drop with a clear
  error pointing at both fixes; document the same in --no-drag-and-drop
  help.
- Add DirectoryShare.collect() tests covering empty, named, drop-zone-only,
  unnamed-conflict, and custom-mount-tag cases.
2026-05-20 10:15:08 +02:00
Dal Rupnik 8546f07ea8 feat: Implement drag and drop functionality 2026-05-20 10:15:08 +02:00
Nikolay Edigaryev 605234b5dd
Mention macOS Tahoe everywhere instead of macOS Sequoia (#1208)
* Mention macOS Tahoe everywhere instead of macOS Sequoia

* Fix spurious rename
2026-03-02 08:50:29 -05:00
Fedor Korotkov faa40b6832
Remove disk v1 support (#1204)
* Remove disk v1 support

* fix: address PR review feedback

- add explicit error for legacy disk.v1 media type during pull
- include actionable re-push guidance in runtime error

🤖 Generated with [Codex](https://chatgpt.com/codex)

Co-Authored-By: Codex <codex@openai.com>

* Re-use legacyDiskV1MediaType in error message

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>
2026-02-25 14:34:25 +00:00
Nikolay Edigaryev d45ef38cf7
StdinCredentials: increase maxCharacters to 8,192 (#1203) 2026-02-23 18:55:52 +01:00
Nikolay Edigaryev e26b376d51
tart list: remove "SizeOnDisk" and add "Accessed" field (#1202)
* tart list: introduce "Accessed" field to show last accessed date of a VM

* tart list: remove "SizeOnDisk" field as it's unused
2026-02-23 18:55:36 +01:00
Nikolay Edigaryev 863e3c2925
Bind and connect to Unix domain sockets using relative paths (#1192) 2026-02-05 15:51:14 +01:00
Nikolay Edigaryev f1aa591935
OpenTelemetry: set default resources, service.name and service.version (#1184)
* OpenTelemetry: set default resources, service.name and service.version

* Ensure that service name and version resources are set
2026-01-27 16:17:05 +01:00
Fedor Korotkov 6189dc23af
Fix VM window not appearing on tart run (#1183)
Restore the applicationDidFinishLaunching method that was accidentally
removed in commit b1e88e1 ("tart run: do not remove 'Edit' menu as its
not present anymore").

That commit intended to remove the Edit menu removal code (since the
menu no longer exists), but also removed the crucial activation code:
- setActivationPolicy(.regular) - tells macOS this is a GUI app
- activate(ignoringOtherApps:) - brings the window to the foreground

Without these calls, the VM runs fine (SSH works) but no window appears
on screen.

Fixes #1181

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 15:16:33 -05:00
Nikolay Edigaryev e0147448a8
OpenTelemetry: only initialize tracing when TRACEPARENT env. var. is set (#1182) 2026-01-23 14:57:53 +01:00
Nikolay Edigaryev 7038c45f8b
Switch to OpenTelemetry (#1179)
* Switch to OpenTelemetry

* Integration tests in Golang
2026-01-23 12:04:21 +01:00
Nikolay Edigaryev 44892c5def
Refactor "diskutil create" and "diskutil info" into a separate class (#1172)
* Show true ASIF disk sizes

* Use older sizeGB()
2026-01-22 13:00:27 +01:00
Nikolay Edigaryev 20dcfc83f2
Disable Sentry's app launch profiling (#1164)
And access SentrySDK only when SENTRY_DSN is set.
2025-11-10 23:50:32 +04:00
Nikolay Edigaryev 68ffa6c5e4
tart set: support optional "pt" and "px" units for "--display" argument (#1155)
* tart set: support optional "pt" and "px" units for "--display" argument

* Don't forget to update "unit" too
2025-10-21 21:35:42 -04:00
Nikolay Edigaryev 1b091e9db0
tart run: introduce new "--net-softnet-block" command-line argument (#1156) 2025-10-21 21:14:43 +04:00
Eric Kolve 90d9500133
chore: adding no-keyboard, no-pointer options for run (#1091) 2025-10-09 15:29:28 -04:00
Nikolay Edigaryev d762fe6fc1
tart run: do not recommend running "tart run" as root (#1147) 2025-10-08 12:44:05 +00:00
Stefan Mitterrutzner eff964b62a
Avoid duplicate progress updates in CI logs (#1140)
* Avoid duplicate progress updates in CI logs

* Update Sources/tart/Logging/ProgressObserver.swift

Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>

---------

Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>
2025-09-29 15:57:39 +04:00
Nikolay Edigaryev e3ee2da2fd
Validate custom TART_HOME and provide a human-friendly error message (#1138)
* Validate custom TART_HOME and provide a human-friendly error message

* Safer way to calculate "descendingURLs"
2025-09-25 20:44:57 +04:00
Nikolay Edigaryev df100f1ca2
Improve credential provider errors (#1133) 2025-09-22 22:57:05 +04:00
Fedor Korotkov 02bf5651e7
tart clone: make pruning limit configurable (#1126)
* tart clone: make pruning limit configurable

* Fixed compilation
2025-09-14 12:38:57 -04:00
Fedor Korotkov 96c89ad76e
tart clone: cap automatic pruning at 100 GB (#1124) 2025-09-14 09:40:58 -04:00
Nikolay Edigaryev b78fa6ba1c
ASIF is available only starting from macOS 26 (Tahoe) (#1096)
* ASIF is available only starting from macOS 26 (Tahoe)

* Remove testRawFormatIsAlwaysSupported() test

* Fix testASIFFormatSupport() test to check for macOS 26+
2025-09-14 09:40:06 -04:00
Nikolay Edigaryev e443cfa9a2
tart exec: do not attempt to call TTY-related methods when no -t is set (#1122) 2025-09-12 19:17:17 +04:00
Nikolay Edigaryev e35c13425e
tart exec: handle input redirection of regular files (#1106) 2025-07-14 19:49:12 +04:00
Nikolay Edigaryev 294c5fc5e5
Upgrade Swift Argument Parser to 1.6.1 (#1103)
* Upgrade Swift Argument Parser to 1.6.1

* Remove ArgumentParser workaround
2025-07-08 00:19:50 +04:00
Fedor Korotkov a2972aa4d9
feat: prioritize pruning of old SHA when pulling updated tags (#1102)
* feat: prioritize pruning of old SHA when pulling updated tags

When pulling a new version of a tagged image (e.g., ghcr.io/cirruslabs/macos-runner:sonoma),
set the access date of the previous SHA to epoch time (1970-01-01). This ensures that the
old SHA will be prioritized for pruning, even if it was accessed more recently than other
cached images.

This helps manage disk space more efficiently by automatically cleaning up superseded
versions of frequently-updated tagged images.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* format

* Review comments

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-07-07 08:52:05 +00:00
Fedor Korotkov 3a6c5fb81d
feat: Add disk image format selection with ASIF support (#1094)
* feat: Add disk image format selection with ASIF support

* fixed goreleaser-pro

* Fix ASIF disk format compatibility issues

- Use .uncached caching mode for ASIF disks to avoid Virtualization framework compatibility issues
- Improve caching mode selection logic for better maintainability
- Fix compiler warning by changing var to let for attachment variable

This resolves VM startup failures when using ASIF disk format by ensuring proper disk attachment configuration.

* Update goreleaser installation to use tap-specific formula

Change from 'brew install --cask goreleaser-pro' to 'brew install --cask goreleaser/tap/goreleaser-pro' for proper installation from the official goreleaser tap.

* Remove VS Code configuration and add to gitignore

- Remove .vscode/launch.json from repository
- Add .vscode/ to .gitignore to prevent VS Code settings from being tracked

* Implement ASIF disk resize using diskutil

- Add support for resizing ASIF disk images using diskutil image resize
- Detect disk format from VM config and route to appropriate resize method
- Use diskutil image info to get current ASIF disk size and validate resize
- Remove restriction that prevented ASIF disk resizing in Set command
- Add FailedToResizeDisk error case for proper error handling
- Maintain backward compatibility with raw disk resizing
- Add comprehensive size validation to prevent data loss

* Update Sources/tart/Commands/Create.swift

Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>

* Update Sources/tart/DiskImageFormat.swift

Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>

* Update Sources/tart/DiskImageFormat.swift

Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>

* Fix test naming and remove redundant test cases

- Rename testFormatArgument to testCaseInsensitivity for clarity
- Remove redundant 'raw' and 'invalid' test cases already covered in testFormatFromString
- Remove testFormatDescriptions test as it's not very useful

Addresses review comment: https://github.com/cirruslabs/tart/pull/1094#discussion_r2152093510

* Remove canCreate property and simplify DiskImageFormat

- Remove canCreate property since it's the same as isSupported
- Remove description property entirely as it's not used
- Fix displayName for RAW format (remove UDIF reference)
- Remove checkDiskutilASIFSupport helper function

Addresses review comments:
- https://github.com/cirruslabs/tart/pull/1094#discussion_r2152109450
- https://github.com/cirruslabs/tart/pull/1094#discussion_r2152115610
- https://github.com/cirruslabs/tart/pull/1094#discussion_r2152124330

* Update Create command validation and help text

- Simplify ArgumentParser help text to let it show possible values automatically
- Remove canCreate validation since property was removed
- Simplify error message for unsupported disk formats

Addresses review comment: https://github.com/cirruslabs/tart/pull/1094#discussion_r2152113480

* Add disk format validation to Run command

- Add validation to ensure ASIF disk format is supported on current system
- Check disk format compatibility before attempting to run VM

Addresses review comment: https://github.com/cirruslabs/tart/pull/1094#discussion_r2152109450

* Use proper namespaced constant for OCI label

- Add diskFormatLabelAnnotation constant in Manifest.swift
- Use org.cirruslabs.tart.disk.format namespace for consistency
- Use variable shadowing instead of new variable name for labels

Addresses review comment: https://github.com/cirruslabs/tart/pull/1094#discussion_r2152163515

* Remove special ASIF caching mode

- Remove .uncached caching mode for ASIF disks
- Use default caching logic for all disk formats
- Testing shows .cached mode works fine on macOS 26.0

Addresses review comment: https://github.com/cirruslabs/tart/pull/1094#discussion_r2152133589

* Improve code structure in VMDirectory

- Use guard let instead of nested if let for better readability
- Reduce nesting in resizeASIFDisk function
- Improve error handling flow

Addresses review comment: https://github.com/cirruslabs/tart/pull/1094#discussion_r2152141916

* diskFormatLabel

* reverted caching mode

* Use PropertyListDecoder

---------

Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>
2025-06-19 18:27:30 +04:00
Nikolay Edigaryev 5793935317
tart ip: implement --resolver=agent (#1095)
* tart ip: implement --resolver=agent

* CI: fix GoReleaser installation
2025-06-19 13:07:06 +04:00
Nikolay Edigaryev 8dc8b644b2 tart exec: do not limit RPC call duration to 1 second 2025-06-11 20:15:18 +02:00
Nikolay Edigaryev b625c04131
tart exec: make sure <name> goes after flags like -i and -t in --help (#1090) 2025-06-11 21:58:30 +04:00
Nikolay Edigaryev 8539b8faae
Delay Sentry initialization until after we parse the CLI arguments (#1085) 2025-05-30 17:24:19 +04:00
Nikolay Edigaryev 71159373e5
tart run: allow "--dir" with "--suspendable" (#1082) 2025-05-30 17:24:09 +04:00
fedor 1cbc1e2cda Suspendable VMs now support consoles 2025-05-28 17:09:54 -04:00
Nikolay Edigaryev 0187834c34
tart exec: explain that Tart Guest Agent is required (#1078)
* tart exec: explain that Tart Guest Agent is required

Also handle decrease the connection timeout to 1 second
and provide a hint to the user.

* execute() can be made private

* Include error.localizedDescription
2025-05-27 12:57:56 +04:00
Nikolay Edigaryev dfbdb5559c
Introduce "tart exec" command as an alternative to SSH (#1074)
* Introduce "tart exec" command as an alternative to SSH

* Simplify control socket machinery by using NIO async/await primitives

* No reason to print the "vm" object directly, just refer to it as "VM"

* Log to Apple’s Unified Logging System
2025-05-22 17:28:14 +04:00
Nikolay Edigaryev 40ab5c3af4
Fix unescaped commas in generated ArgumentParser completions (#1066)
* Fix unescaped commas in generated ArgumentParser completions

* Improve completion hints
2025-05-06 14:24:43 +04:00
fedor 280a31f707 Update docs, examples and CI to Sequoia 2025-05-04 20:49:23 -04:00
Nikolay Edigaryev 8d49404337
Enable clipboard sharing on macOS too (#1046)
* Enable clipboard sharing on macOS too

And document which packages need to be installed on these operating
systems.

* We now use Tart Guest Agent

Co-authored-by: Fedor Korotkov <fedor.korotkov@gmail.com>

---------

Co-authored-by: Fedor Korotkov <fedor.korotkov@gmail.com>
2025-04-29 21:15:03 +04:00