Add option to specify download directory for assets (#989)

Co-authored-by: Oleksii Holub <1935960+Tyrrrz@users.noreply.github.com>
pull/1003/head
Lucas LaBuff 2 years ago committed by GitHub
parent 53ff8ba430
commit 95115f3e99
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -98,6 +98,20 @@ public abstract class ExportCommandBase : TokenCommandBase
)]
public bool ShouldReuseAssets { get; init; }
private readonly string? _assetsPath;
[CommandOption(
"media-dir",
Description = "Download assets to this directory."
)]
public string? AssetsPath
{
get => _assetsPath;
// Handle ~/ in paths on Unix systems
// https://github.com/Tyrrrz/DiscordChatExporter/pull/903
init => _assetsPath = value is not null ? Path.GetFullPath(value) : null;
}
[CommandOption(
"dateformat",
Description = "Format used when writing dates."
@ -124,6 +138,14 @@ public abstract class ExportCommandBase : TokenCommandBase
);
}
// Assets directory should only be specified when the download assets option is set
if (!string.IsNullOrWhiteSpace(AssetsPath) && !ShouldDownloadAssets)
{
throw new CommandException(
"Option --media-dir cannot be used without --media."
);
}
// Make sure the user does not try to export all channels into a single file.
// Output path must either be a directory, or contain template tokens.
// https://github.com/Tyrrrz/DiscordChatExporter/issues/799
@ -172,6 +194,7 @@ public abstract class ExportCommandBase : TokenCommandBase
guild,
channel,
OutputPath,
AssetsPath,
ExportFormat,
After,
Before,

@ -92,13 +92,19 @@ internal class ExportContext
try
{
var filePath = await _assetDownloader.DownloadAsync(url, cancellationToken);
var absoluteFilePath = await _assetDownloader.DownloadAsync(url, cancellationToken);
// We want relative path so that the output files can be copied around without breaking.
// Base directory path may be null if the file is stored at the root or relative to working directory.
var relativeFilePath = !string.IsNullOrWhiteSpace(Request.OutputBaseDirPath)
? Path.GetRelativePath(Request.OutputBaseDirPath, filePath)
: filePath;
? Path.GetRelativePath(Request.OutputBaseDirPath, absoluteFilePath)
: absoluteFilePath;
// If the assets path is outside of the export directory, fall back to absolute path
var filePath = relativeFilePath.StartsWith("..")
? absoluteFilePath
: relativeFilePath;
// HACK: for HTML, we need to format the URL properly
if (Request.Format is ExportFormat.HtmlDark or ExportFormat.HtmlLight)
@ -106,13 +112,14 @@ internal class ExportContext
// Need to escape each path segment while keeping the directory separators intact
return string.Join(
Path.AltDirectorySeparatorChar,
relativeFilePath
filePath
.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
.Select(Uri.EscapeDataString)
.Select(x => x.Replace("%3A", ":"))
);
}
return relativeFilePath;
return filePath;
}
// Try to catch only exceptions related to failed HTTP requests
// https://github.com/Tyrrrz/DiscordChatExporter/issues/332

@ -14,6 +14,7 @@ public partial record ExportRequest(
Guild Guild,
Channel Channel,
string OutputPath,
string? AssetsPath,
ExportFormat Format,
Snowflake? After,
Snowflake? Before,
@ -36,7 +37,18 @@ public partial record ExportRequest(
public string OutputBaseDirPath => Path.GetDirectoryName(OutputBaseFilePath) ?? OutputPath;
public string OutputAssetsDirPath => $"{OutputBaseFilePath}_Files{Path.DirectorySeparatorChar}";
private string? _outputAssetsDirPath;
public string OutputAssetsDirPath => _outputAssetsDirPath ??= (
AssetsPath is not null
? EvaluateTemplateTokens(
AssetsPath,
Guild,
Channel,
After,
Before
)
: $"{OutputBaseFilePath}_Files{Path.DirectorySeparatorChar}"
);
}
public partial record ExportRequest
@ -83,17 +95,15 @@ public partial record ExportRequest
return PathEx.EscapeFileName(buffer.ToString());
}
private static string GetOutputBaseFilePath(
private static string EvaluateTemplateTokens(
string path,
Guild guild,
Channel channel,
string outputPath,
ExportFormat format,
Snowflake? after = null,
Snowflake? before = null)
Snowflake? after,
Snowflake? before)
{
// Format path
var actualOutputPath = Regex.Replace(
outputPath,
return Regex.Replace(
path,
"%.",
m => PathEx.EscapeFileName(m.Value switch
{
@ -110,8 +120,18 @@ public partial record ExportRequest
"%d" => DateTimeOffset.Now.ToString("yyyy-MM-dd"),
"%%" => "%",
_ => m.Value
})
);
}));
}
private static string GetOutputBaseFilePath(
Guild guild,
Channel channel,
string outputPath,
ExportFormat format,
Snowflake? after = null,
Snowflake? before = null)
{
var actualOutputPath = EvaluateTemplateTokens(outputPath, guild, channel, after, before);
// Output is a directory
if (Directory.Exists(actualOutputPath) || string.IsNullOrWhiteSpace(Path.GetExtension(actualOutputPath)))

@ -186,6 +186,7 @@ public class DashboardViewModel : PropertyChangedBase
dialog.Guild!,
channel,
dialog.OutputPath!,
null,
dialog.SelectedFormat,
dialog.After?.Pipe(Snowflake.FromDate),
dialog.Before?.Pipe(Snowflake.FromDate),

Loading…
Cancel
Save