aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDan Engelbrecht <[email protected]>2025-04-23 17:16:16 +0200
committerGitHub Enterprise <[email protected]>2025-04-23 17:16:16 +0200
commit2ec01ef9c6e07b77afa33fe3db6335f231c78006 (patch)
treee2224d7859c4ce88defc046f9e55b13a3f9fa41f
parentparse system dir for builds (#365) (diff)
downloadzen-2ec01ef9c6e07b77afa33fe3db6335f231c78006.tar.xz
zen-2ec01ef9c6e07b77afa33fe3db6335f231c78006.zip
zen wipe command (#366)
- Feature: New `zen wipe` command for fast cleaning of directories, it will not remove the directory itself, only the content - `--directory` - path to directory to wipe, if the directory does not exist or is empty, no action will be taken - `--keep-readonly` - skip removal of read-only files found in directory, defaults to `true`, set to `false` to remove read-only files - `--quiet` - reduce output to console, defaults to `false` - `--dryrun` - simulate the wipe without removing anything, defaults to `false` - `--yes` - skips prompt to confirm wipe of directory - `--plain-progress` - show progress using plain output - `--verbose` - enable verbose console output - `--boost-workers` - increase the number of worker threads, may cause computer to be less responsive, defaults to `false`
-rw-r--r--CHANGELOG.md9
-rw-r--r--src/zen/cmds/builds_cmd.cpp2
-rw-r--r--src/zen/cmds/wipe_cmd.cpp575
-rw-r--r--src/zen/cmds/wipe_cmd.h36
-rw-r--r--src/zen/zen.cpp3
5 files changed, 624 insertions, 1 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f6c434412..f4dca4c21 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,15 @@
- Bugfix: Use proper FindClose call when using fallback when getting file attributes on windows
- Bugfix: Fixed race condition at final chunks when downloading multipart blobs which could lead to corruption and/or crash
- Bugfix: Fixed BigInt conversion error affecting the tree view in the web UI
+- Feature: New `zen wipe` command for fast cleaning of directories, it will not remove the directory itself, only the content
+ - `--directory` - path to directory to wipe, if the directory does not exist or is empty, no action will be taken
+ - `--keep-readonly` - skip removal of read-only files found in directory, defaults to `true`, set to `false` to remove read-only files
+ - `--quiet` - reduce output to console, defaults to `false`
+ - `--dryrun` - simulate the wipe without removing anything, defaults to `false`
+ - `--yes` - skips prompt to confirm wipe of directory
+ - `--plain-progress` - show progress using plain output
+ - `--verbose` - enable verbose console output
+ - `--boost-workers` - increase the number of worker threads, may cause computer to be less responsive, defaults to `false`
- Feature: **EXPERIMENTAL** New `--plugins-config` option to load plugins based on transport-sdk.
- It accepts a json of format `[{"name": "%path_to_dll%", "%opt1%": "%val1%", ...}, ]`.
diff --git a/src/zen/cmds/builds_cmd.cpp b/src/zen/cmds/builds_cmd.cpp
index b113ce6d1..6c97645cd 100644
--- a/src/zen/cmds/builds_cmd.cpp
+++ b/src/zen/cmds/builds_cmd.cpp
@@ -8873,7 +8873,7 @@ BuildsCommand::BuildsCommand()
Ops.add_option("",
"",
"boost-workers",
- "Increase the number of worker threads - may cause computer to less responsive",
+ "Increase the number of worker threads - may cause computer to be less responsive",
cxxopts::value(m_BoostWorkerThreads),
"<boostworkers>");
};
diff --git a/src/zen/cmds/wipe_cmd.cpp b/src/zen/cmds/wipe_cmd.cpp
new file mode 100644
index 000000000..2b4e9ab3c
--- /dev/null
+++ b/src/zen/cmds/wipe_cmd.cpp
@@ -0,0 +1,575 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "wipe_cmd.h"
+
+#include <zencore/filesystem.h>
+#include <zencore/fmtutils.h>
+#include <zencore/logging.h>
+#include <zencore/string.h>
+#include <zencore/timer.h>
+#include <zencore/trace.h>
+#include <zenutil/parallellwork.h>
+#include <zenutil/workerpools.h>
+
+#include <signal.h>
+
+#include <iostream>
+
+ZEN_THIRD_PARTY_INCLUDES_START
+#include <tsl/robin_map.h>
+#include <tsl/robin_set.h>
+ZEN_THIRD_PARTY_INCLUDES_END
+
+#if ZEN_PLATFORM_WINDOWS
+# include <zencore/windows.h>
+#else
+# include <fcntl.h>
+# include <sys/file.h>
+# include <sys/stat.h>
+# include <unistd.h>
+#endif
+
+namespace zen {
+
+namespace {
+ static std::atomic<bool> AbortFlag = false;
+ static bool IsVerbose = false;
+ static bool Quiet = false;
+ static bool UsePlainProgress = false;
+ const bool SingleThreaded = false;
+ bool BoostWorkerThreads = true;
+
+ WorkerThreadPool& GetIOWorkerPool()
+ {
+ return SingleThreaded ? GetSyncWorkerPool()
+ : BoostWorkerThreads ? GetLargeWorkerPool(EWorkloadType::Burst)
+ : GetMediumWorkerPool(EWorkloadType::Burst);
+ }
+
+#define ZEN_CONSOLE_VERBOSE(fmtstr, ...) \
+ if (IsVerbose) \
+ { \
+ ZEN_CONSOLE_LOG(zen::logging::level::Info, fmtstr, ##__VA_ARGS__); \
+ }
+
+ static void SignalCallbackHandler(int SigNum)
+ {
+ if (SigNum == SIGINT)
+ {
+ AbortFlag = true;
+ }
+#if ZEN_PLATFORM_WINDOWS
+ if (SigNum == SIGBREAK)
+ {
+ AbortFlag = true;
+ }
+#endif // ZEN_PLATFORM_WINDOWS
+ }
+
+ bool IsReadOnly(uint32_t Attributes)
+ {
+#if ZEN_PLATFORM_WINDOWS
+ return IsFileAttributeReadOnly(Attributes);
+#else
+ return IsFileModeReadOnly(Attributes);
+#endif
+ }
+
+ bool IsFileWithRetry(const std::filesystem::path& Path)
+ {
+ std::error_code Ec;
+ bool Result = IsFile(Path, Ec);
+ for (size_t Retries = 0; Ec && Retries < 3; Retries++)
+ {
+ Sleep(100 + int(Retries * 50));
+ Ec.clear();
+ Result = IsFile(Path, Ec);
+ }
+ if (Ec)
+ {
+ zen::ThrowSystemError(Ec.value(), Ec.message());
+ }
+ return Result;
+ }
+
+ bool SetFileReadOnlyWithRetry(const std::filesystem::path& Path, bool ReadOnly)
+ {
+ std::error_code Ec;
+ bool Result = SetFileReadOnly(Path, ReadOnly, Ec);
+ for (size_t Retries = 0; Ec && Retries < 3; Retries++)
+ {
+ Sleep(100 + int(Retries * 50));
+ if (!IsFileWithRetry(Path))
+ {
+ return false;
+ }
+ Ec.clear();
+ Result = SetFileReadOnly(Path, ReadOnly, Ec);
+ }
+ if (Ec)
+ {
+ zen::ThrowSystemError(Ec.value(), Ec.message());
+ }
+ return Result;
+ }
+
+ void RemoveFileWithRetry(const std::filesystem::path& Path)
+ {
+ std::error_code Ec;
+ RemoveFile(Path, Ec);
+ for (size_t Retries = 0; Ec && Retries < 3; Retries++)
+ {
+ Sleep(100 + int(Retries * 50));
+ if (!IsFileWithRetry(Path))
+ {
+ return;
+ }
+ Ec.clear();
+ RemoveFile(Path, Ec);
+ }
+ if (Ec)
+ {
+ zen::ThrowSystemError(Ec.value(), Ec.message());
+ }
+ }
+
+ void RemoveDirWithRetry(const std::filesystem::path& Path)
+ {
+ std::error_code Ec;
+ RemoveDir(Path, Ec);
+ for (size_t Retries = 0; Ec && Retries < 3; Retries++)
+ {
+ Sleep(100 + int(Retries * 50));
+ if (!IsDir(Path))
+ {
+ return;
+ }
+ Ec.clear();
+ RemoveDir(Path, Ec);
+ }
+ if (Ec)
+ {
+ zen::ThrowSystemError(Ec.value(), Ec.message());
+ }
+ }
+
+ bool CleanDirectory(const std::filesystem::path& Path,
+ std::span<const std::string_view> ExcludeDirectories,
+ bool RemoveReadonly,
+ bool Dryrun)
+ {
+ ZEN_TRACE_CPU("CleanDirectory");
+ Stopwatch Timer;
+
+ ProgressBar Progress(UsePlainProgress);
+
+ std::atomic<bool> CleanWipe = true;
+ std::atomic<uint64_t> DiscoveredItemCount = 0;
+ std::atomic<uint64_t> DeletedItemCount = 0;
+ std::atomic<uint64_t> DeletedByteCount = 0;
+ std::atomic<uint64_t> FailedDeleteCount = 0;
+
+ std::vector<std::filesystem::path> SubdirectoriesToDelete;
+ tsl::robin_map<IoHash, size_t, IoHash::Hasher> SubdirectoriesToDeleteLookup;
+ tsl::robin_set<IoHash, IoHash::Hasher> SubdirectoriesToKeep;
+ RwLock SubdirectoriesLock;
+
+ auto AddFoundDirectory = [&](std::filesystem::path Directory, bool Keep) -> bool {
+ bool Added = false;
+ if (Keep)
+ {
+ bool IsLeaf = true;
+ while (Directory != Path)
+ {
+ const std::string DirectoryString = Directory.generic_string();
+ IoHash DirectoryNameHash = IoHash::HashBuffer(DirectoryString.data(), DirectoryString.length());
+ RwLock::ExclusiveLockScope _(SubdirectoriesLock);
+ if (auto It = SubdirectoriesToKeep.find(DirectoryNameHash); It == SubdirectoriesToKeep.end())
+ {
+ SubdirectoriesToKeep.insert(DirectoryNameHash);
+ if (IsLeaf)
+ {
+ Added = true;
+ }
+ }
+ else
+ {
+ break;
+ }
+ Directory = Directory.parent_path();
+ IsLeaf = false;
+ }
+ }
+ else
+ {
+ bool IsLeaf = true;
+ while (Directory != Path)
+ {
+ const std::string DirectoryString = Directory.generic_string();
+ IoHash DirectoryNameHash = IoHash::HashBuffer(DirectoryString.data(), DirectoryString.length());
+ RwLock::ExclusiveLockScope _(SubdirectoriesLock);
+ if (SubdirectoriesToKeep.contains(DirectoryNameHash))
+ {
+ break;
+ }
+ if (auto It = SubdirectoriesToDeleteLookup.find(DirectoryNameHash); It == SubdirectoriesToDeleteLookup.end())
+ {
+ SubdirectoriesToDeleteLookup.insert({DirectoryNameHash, SubdirectoriesToDelete.size()});
+ SubdirectoriesToDelete.push_back(Directory);
+ if (IsLeaf)
+ {
+ Added = true;
+ }
+ }
+ else
+ {
+ break;
+ }
+ Directory = Directory.parent_path();
+ IsLeaf = false;
+ }
+ }
+ return Added;
+ };
+
+ ParallellWork Work(AbortFlag);
+
+ struct AsyncVisitor : public GetDirectoryContentVisitor
+ {
+ AsyncVisitor(const std::filesystem::path& InPath,
+ std::atomic<bool>& InCleanWipe,
+ std::atomic<uint64_t>& InDiscoveredItemCount,
+ std::atomic<uint64_t>& InDeletedItemCount,
+ std::atomic<uint64_t>& InDeletedByteCount,
+ std::atomic<uint64_t>& InFailedDeleteCount,
+ std::span<const std::string_view> InExcludeDirectories,
+ bool InRemoveReadonly,
+ bool InDryrun,
+ const std::function<bool(std::filesystem::path, bool)>& InAddFoundDirectoryFunc)
+ : Path(InPath)
+ , CleanWipe(InCleanWipe)
+ , DiscoveredItemCount(InDiscoveredItemCount)
+ , DeletedItemCount(InDeletedItemCount)
+ , DeletedByteCount(InDeletedByteCount)
+ , FailedDeleteCount(InFailedDeleteCount)
+ , ExcludeDirectories(InExcludeDirectories)
+ , RemoveReadonly(InRemoveReadonly)
+ , Dryrun(InDryrun)
+ , AddFoundDirectoryFunc(InAddFoundDirectoryFunc)
+ {
+ }
+ virtual void AsyncVisitDirectory(const std::filesystem::path& RelativeRoot, DirectoryContent&& Content) override
+ {
+ ZEN_TRACE_CPU("CleanDirectory_AsyncVisitDirectory");
+ if (!AbortFlag)
+ {
+ if (!RelativeRoot.empty())
+ {
+ DiscoveredItemCount++;
+ }
+ if (!Content.FileNames.empty())
+ {
+ DiscoveredItemCount += Content.FileNames.size();
+
+ const std::string RelativeRootString = RelativeRoot.generic_string();
+ bool RemoveContent = true;
+ for (const std::string_view ExcludeDirectory : ExcludeDirectories)
+ {
+ if (RelativeRootString.starts_with(ExcludeDirectory))
+ {
+ if (RelativeRootString.length() > ExcludeDirectory.length())
+ {
+ const char MaybePathDelimiter = RelativeRootString[ExcludeDirectory.length()];
+ if (MaybePathDelimiter == '/' || MaybePathDelimiter == '\\' ||
+ MaybePathDelimiter == std::filesystem::path::preferred_separator)
+ {
+ RemoveContent = false;
+ break;
+ }
+ }
+ else
+ {
+ RemoveContent = false;
+ break;
+ }
+ }
+ }
+
+ const std::filesystem::path ParentPath = Path / RelativeRoot;
+ bool KeepDirectory = RelativeRoot.empty();
+
+ if (RemoveContent)
+ {
+ ZEN_TRACE_CPU("DeleteFiles");
+ uint64_t RemovedCount = 0;
+ for (size_t FileIndex = 0; FileIndex < Content.FileNames.size(); FileIndex++)
+ {
+ const std::filesystem::path& FileName = Content.FileNames[FileIndex];
+ const std::filesystem::path FilePath = (ParentPath / FileName).make_preferred();
+ try
+ {
+ const uint32_t Attributes = Content.FileAttributes[FileIndex];
+ const bool IsReadonly = IsReadOnly(Attributes);
+ bool RemoveFile = false;
+ if (IsReadonly)
+ {
+ if (RemoveReadonly)
+ {
+ if (!Dryrun)
+ {
+ SetFileReadOnlyWithRetry(FilePath, false);
+ }
+ RemoveFile = true;
+ }
+ }
+ else
+ {
+ RemoveFile = true;
+ }
+
+ if (RemoveFile)
+ {
+ if (!Dryrun)
+ {
+ RemoveFileWithRetry(FilePath);
+ }
+ DeletedItemCount++;
+ DeletedByteCount += Content.FileSizes[FileIndex];
+ RemovedCount++;
+ ZEN_CONSOLE_VERBOSE("Removed file {}", FilePath);
+ }
+ else
+ {
+ ZEN_CONSOLE_VERBOSE("Skipped readonly file {}", FilePath);
+ KeepDirectory = true;
+ }
+ }
+ catch (const std::exception& Ex)
+ {
+ ZEN_WARN("Failed removing file {}. Reason: {}", FilePath, Ex.what());
+ FailedDeleteCount++;
+ CleanWipe = false;
+ KeepDirectory = true;
+ }
+ }
+ ZEN_CONSOLE_VERBOSE("Removed {} files in {}", RemovedCount, ParentPath);
+ }
+ else
+ {
+ ZEN_CONSOLE_VERBOSE("Skipped removal of {} files in {}", Content.FileNames.size(), ParentPath);
+ }
+ bool Added = AddFoundDirectoryFunc(ParentPath, KeepDirectory);
+ if (Added)
+ {
+ ZEN_CONSOLE_VERBOSE("{} directory {}", KeepDirectory ? "Keeping" : "Removing", ParentPath);
+ }
+ }
+ }
+ }
+ const std::filesystem::path& Path;
+ std::atomic<bool>& CleanWipe;
+ std::atomic<uint64_t>& DiscoveredItemCount;
+ std::atomic<uint64_t>& DeletedItemCount;
+ std::atomic<uint64_t>& DeletedByteCount;
+ std::atomic<uint64_t>& FailedDeleteCount;
+ std::span<const std::string_view> ExcludeDirectories;
+ const bool RemoveReadonly;
+ const bool Dryrun;
+ std::function<bool(std::filesystem::path, bool)> AddFoundDirectoryFunc;
+ } Visitor(Path,
+ CleanWipe,
+ DiscoveredItemCount,
+ DeletedItemCount,
+ DeletedByteCount,
+ FailedDeleteCount,
+ ExcludeDirectories,
+ RemoveReadonly,
+ Dryrun,
+ AddFoundDirectory);
+
+ uint64_t LastUpdateTimeMs = Timer.GetElapsedTimeMs();
+
+ GetDirectoryContent(Path,
+ DirectoryContentFlags::IncludeFiles | DirectoryContentFlags::Recursive |
+ DirectoryContentFlags::IncludeFileSizes | DirectoryContentFlags::IncludeAttributes,
+ Visitor,
+ GetIOWorkerPool(),
+ Work.PendingWork());
+
+ Work.Wait(UsePlainProgress ? 5000 : 200, [&](bool IsAborted, ptrdiff_t PendingWork) {
+ if (Quiet)
+ {
+ return;
+ }
+ ZEN_UNUSED(IsAborted, PendingWork);
+ LastUpdateTimeMs = Timer.GetElapsedTimeMs();
+
+ uint64_t Deleted = DeletedItemCount.load();
+ uint64_t DeletedBytes = DeletedByteCount.load();
+ uint64_t Discovered = DiscoveredItemCount.load();
+ Progress.UpdateState({.Task = "Removing files ",
+ .Details = fmt::format("Found {}, Deleted {} ({})", Discovered, Deleted, NiceBytes(DeletedBytes)),
+ .TotalCount = Discovered,
+ .RemainingCount = Discovered - Deleted},
+ false);
+ });
+
+ std::vector<std::filesystem::path> DirectoriesToDelete;
+ DirectoriesToDelete.reserve(SubdirectoriesToDelete.size());
+ for (auto It : SubdirectoriesToDeleteLookup)
+ {
+ const IoHash& DirHash = It.first;
+ if (auto KeepIt = SubdirectoriesToKeep.find(DirHash); KeepIt == SubdirectoriesToKeep.end())
+ {
+ DirectoriesToDelete.emplace_back(std::move(SubdirectoriesToDelete[It.second]));
+ }
+ }
+
+ std::sort(DirectoriesToDelete.begin(),
+ DirectoriesToDelete.end(),
+ [](const std::filesystem::path& Lhs, const std::filesystem::path& Rhs) {
+ return Lhs.string().length() > Rhs.string().length();
+ });
+
+ for (size_t SubDirectoryIndex = 0; SubDirectoryIndex < DirectoriesToDelete.size(); SubDirectoryIndex++)
+ {
+ ZEN_TRACE_CPU("DeleteDirs");
+ const std::filesystem::path& DirectoryToDelete = DirectoriesToDelete[SubDirectoryIndex];
+ try
+ {
+ if (!Dryrun)
+ {
+ RemoveDirWithRetry(DirectoryToDelete);
+ }
+ ZEN_CONSOLE_VERBOSE("Removed directory {}", DirectoryToDelete);
+ DeletedItemCount++;
+ }
+ catch (const std::exception& Ex)
+ {
+ if (!Quiet)
+ {
+ ZEN_WARN("Failed removing directory {}. Reason: {}", DirectoryToDelete, Ex.what());
+ }
+ CleanWipe = false;
+ FailedDeleteCount++;
+ }
+
+ uint64_t NowMs = Timer.GetElapsedTimeMs();
+ if ((NowMs - LastUpdateTimeMs) >= (UsePlainProgress ? 5000 : 200))
+ {
+ LastUpdateTimeMs = NowMs;
+
+ uint64_t Deleted = DeletedItemCount.load();
+ uint64_t DeletedBytes = DeletedByteCount.load();
+ uint64_t Discovered = DiscoveredItemCount.load();
+ Progress.UpdateState({.Task = "Removing folders",
+ .Details = fmt::format("Found {}, Deleted {} ({})", Discovered, Deleted, NiceBytes(DeletedBytes)),
+ .TotalCount = DirectoriesToDelete.size(),
+ .RemainingCount = DirectoriesToDelete.size() - SubDirectoryIndex},
+ false);
+ }
+ }
+
+ Progress.Finish();
+
+ uint64_t ElapsedTimeMs = Timer.GetElapsedTimeMs();
+ if (!Quiet)
+ {
+ ZEN_CONSOLE("Wiped folder '{}' {} ({}) ({} failed) in {}",
+ Path,
+ DeletedItemCount.load(),
+ NiceBytes(DeletedByteCount.load()),
+ FailedDeleteCount.load(),
+ NiceTimeSpanMs(ElapsedTimeMs));
+ }
+ if (FailedDeleteCount.load() > 0)
+ {
+ throw std::runtime_error(fmt::format("Failed to delete {} files/directories in '{}'", FailedDeleteCount.load(), Path));
+ }
+ return CleanWipe;
+ }
+} // namespace
+
+WipeCommand::WipeCommand()
+{
+ m_Options.add_options()("h,help", "Print help");
+ m_Options.add_option("", "d", "directory", "Directory to wipe", cxxopts::value(m_Directory), "<directory>");
+ m_Options.add_option("", "r", "keep-readonly", "Leave read-only files", cxxopts::value(m_KeepReadOnlyFiles), "<keepreadonly>");
+ m_Options.add_option("", "q", "quiet", "Reduce output to console", cxxopts::value(m_Quiet), "<quiet>");
+ m_Options.add_option("", "y", "yes", "Don't query for confirmation", cxxopts::value(m_Yes), "<yes>");
+ m_Options.add_option("", "", "dryrun", "Do a dry run without deleting anything", cxxopts::value(m_Dryrun), "<dryrun>");
+ m_Options.add_option("output", "", "plain-progress", "Show progress using plain output", cxxopts::value(m_PlainProgress), "<progress>");
+ m_Options.add_option("output", "", "verbose", "Enable verbose console output", cxxopts::value(m_Verbose), "<verbose>");
+ m_Options.add_option("",
+ "",
+ "boost-workers",
+ "Increase the number of worker threads - may cause computer to be less responsive",
+ cxxopts::value(m_BoostWorkerThreads),
+ "<boostworkers>");
+
+ m_Options.parse_positional({"directory"});
+}
+
+WipeCommand::~WipeCommand() = default;
+
+int
+WipeCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
+{
+ ZEN_UNUSED(GlobalOptions);
+
+ signal(SIGINT, SignalCallbackHandler);
+#if ZEN_PLATFORM_WINDOWS
+ signal(SIGBREAK, SignalCallbackHandler);
+#endif // ZEN_PLATFORM_WINDOWS
+
+ if (!ZenCmdBase::ParseOptions(argc, argv))
+ {
+ return 0;
+ }
+
+ Quiet = m_Quiet;
+ IsVerbose = m_Verbose;
+ UsePlainProgress = IsVerbose || m_PlainProgress;
+ BoostWorkerThreads = m_BoostWorkerThreads;
+
+ MakeSafeAbsolutePathÍnPlace(m_Directory);
+
+ if (!IsDir(m_Directory))
+ {
+ return 0;
+ }
+
+ while (!m_Yes)
+ {
+ const std::string Prompt = fmt::format("Do you want to wipe directory '{}'? (yes/no) ", m_Directory);
+ printf("%s", Prompt.c_str());
+ std::string Reponse;
+ std::getline(std::cin, Reponse);
+ Reponse = ToLower(Reponse);
+ if (Reponse == "y" || Reponse == "yes")
+ {
+ m_Yes = true;
+ }
+ else if (Reponse == "n" || Reponse == "no")
+ {
+ return 0;
+ }
+ }
+
+ try
+ {
+ CleanDirectory(m_Directory, {}, !m_KeepReadOnlyFiles, m_Dryrun);
+ }
+ catch (std::exception& Ex)
+ {
+ if (!m_Quiet)
+ {
+ ZEN_ERROR("{}", Ex.what());
+ }
+ return 3;
+ }
+
+ return 0;
+}
+
+} // namespace zen
diff --git a/src/zen/cmds/wipe_cmd.h b/src/zen/cmds/wipe_cmd.h
new file mode 100644
index 000000000..0e910bb81
--- /dev/null
+++ b/src/zen/cmds/wipe_cmd.h
@@ -0,0 +1,36 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include "../zen.h"
+
+namespace zen {
+
+/** Wipe directories
+ */
+class WipeCommand : public ZenCmdBase
+{
+public:
+ static constexpr char Name[] = "wipe";
+ static constexpr char Description[] = "Wipe the contents of a directory";
+
+ WipeCommand();
+ ~WipeCommand();
+
+ virtual cxxopts::Options& Options() override { return m_Options; }
+ virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override;
+ virtual ZenCmdCategory& CommandCategory() const override { return g_UtilitiesCategory; }
+
+private:
+ cxxopts::Options m_Options{Name, Description};
+ std::filesystem::path m_Directory;
+ bool m_KeepReadOnlyFiles = true;
+ bool m_Quiet = false;
+ bool m_Yes = false;
+ bool m_PlainProgress = false;
+ bool m_Verbose = false;
+ bool m_Dryrun = false;
+ bool m_BoostWorkerThreads = false;
+};
+
+} // namespace zen
diff --git a/src/zen/zen.cpp b/src/zen/zen.cpp
index 5ce0a89ec..e442f8a4b 100644
--- a/src/zen/zen.cpp
+++ b/src/zen/zen.cpp
@@ -23,6 +23,7 @@
#include "cmds/up_cmd.h"
#include "cmds/version_cmd.h"
#include "cmds/vfs_cmd.h"
+#include "cmds/wipe_cmd.h"
#include "cmds/workspaces_cmd.h"
#include <zencore/callstack.h>
@@ -557,6 +558,7 @@ main(int argc, char** argv)
UpCommand UpCmd;
VersionCommand VersionCmd;
VfsCommand VfsCmd;
+ WipeCommand WipeCmd;
WorkspaceCommand WorkspaceCmd;
WorkspaceShareCommand WorkspaceShareCmd;
@@ -613,6 +615,7 @@ main(int argc, char** argv)
{"version", &VersionCmd, "Get zen server version"},
{"vfs", &VfsCmd, "Manage virtual file system"},
{"flush", &FlushCmd, "Flush storage"},
+ {WipeCommand::Name, &WipeCmd, WipeCommand::Description},
{WorkspaceCommand::Name, &WorkspaceCmd, WorkspaceCommand::Description},
{WorkspaceShareCommand::Name, &WorkspaceShareCmd, WorkspaceShareCommand::Description},
// clang-format on