mirror of
https://github.com/revenz/FileFlowsPlugins.git
synced 2025-12-20 23:19:43 -06:00
FF-1721: New plugin Nextcloud
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -39,6 +39,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pushbullet", "Pushbullet\Pu
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Web", "Web\Web.csproj", "{F9AEA7E3-32F7-450D-A9A4-BFB95E6CC899}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nextcloud", "Nextcloud\Nextcloud.csproj", "{A36DD7D5-12BE-4B79-82AA-526E52DC0A3A}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -117,6 +119,10 @@ Global
|
||||
{F9AEA7E3-32F7-450D-A9A4-BFB95E6CC899}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F9AEA7E3-32F7-450D-A9A4-BFB95E6CC899}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F9AEA7E3-32F7-450D-A9A4-BFB95E6CC899}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A36DD7D5-12BE-4B79-82AA-526E52DC0A3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A36DD7D5-12BE-4B79-82AA-526E52DC0A3A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A36DD7D5-12BE-4B79-82AA-526E52DC0A3A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A36DD7D5-12BE-4B79-82AA-526E52DC0A3A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
17
Nextcloud/ExtensionMethods.cs
Normal file
17
Nextcloud/ExtensionMethods.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace FileFlows.Nextcloud;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods
|
||||
/// </summary>
|
||||
internal static class ExtensionMethods
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns an empty string as null, otherwise returns the original string
|
||||
/// </summary>
|
||||
/// <param name="str">the input string</param>
|
||||
/// <returns>the string or null if empty</returns>
|
||||
public static string? EmptyAsNull(this string str)
|
||||
{
|
||||
return str == string.Empty ? null : str;
|
||||
}
|
||||
}
|
||||
88
Nextcloud/FlowElements/UploadToNextcloud.cs
Normal file
88
Nextcloud/FlowElements/UploadToNextcloud.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using FileFlows.Nextcloud.Helpers;
|
||||
|
||||
namespace FileFlows.Nextcloud.FlowElements;
|
||||
|
||||
/// <summary>
|
||||
/// Uploads a file to Nextcloud
|
||||
/// </summary>
|
||||
public class UploadToNextcloud : Node
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override int Inputs => 1;
|
||||
/// <inheritdoc />
|
||||
public override int Outputs => 1;
|
||||
/// <inheritdoc />
|
||||
public override FlowElementType Type => FlowElementType.Process;
|
||||
/// <inheritdoc />
|
||||
public override string CustomColor => "#0082c9";
|
||||
/// <inheritdoc />
|
||||
public override string Icon => "svg:nextcloud";
|
||||
/// <inheritdoc />
|
||||
public override string HelpUrl => "https://fileflows.com/docs/plugins/nextcloud/upload-to-next-cloud";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the destination path
|
||||
/// </summary>
|
||||
[Folder(1)]
|
||||
public string DestinationPath { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the file to unpack
|
||||
/// </summary>
|
||||
[TextVariable(2)]
|
||||
public string File { get; set; } = null!;
|
||||
|
||||
public override int Execute(NodeParameters args)
|
||||
{
|
||||
var settings = args.GetPluginSettings<PluginSettings>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(settings?.Url))
|
||||
{
|
||||
args.FailureReason = "No Nextcloud URL set";
|
||||
args.Logger?.ELog(args.FailureReason);
|
||||
return -1;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(settings?.Username))
|
||||
{
|
||||
args.FailureReason = "No Nextcloud User set";
|
||||
args.Logger?.ELog(args.FailureReason);
|
||||
return -1;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(settings?.Password))
|
||||
{
|
||||
args.FailureReason = "No Nextcloud Password set";
|
||||
args.Logger?.ELog(args.FailureReason);
|
||||
return -1;
|
||||
}
|
||||
|
||||
var file = args.ReplaceVariables(File ?? string.Empty, stripMissing: true)?.EmptyAsNull() ?? args.WorkingFile;
|
||||
var destination = args.ReplaceVariables(DestinationPath ?? string.Empty, stripMissing: true);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(destination))
|
||||
{
|
||||
args.FailureReason = "No Destination set";
|
||||
args.Logger?.ELog(args.FailureReason);
|
||||
return -1;
|
||||
}
|
||||
|
||||
var local = args.FileService.GetLocalPath(file);
|
||||
if (local.Failed(out var error))
|
||||
{
|
||||
args.FailureReason = error;
|
||||
args.Logger?.ELog(args.FailureReason);
|
||||
return -1;
|
||||
}
|
||||
|
||||
var uploader = new NextcloudUploader(args.Logger!, settings.Url, settings.Username, settings.Password);
|
||||
var result = uploader.UploadFile(local.Value, destination);
|
||||
if(result.Failed(out error))
|
||||
{
|
||||
args.FailureReason = error;
|
||||
args.Logger?.ELog(args.FailureReason);
|
||||
return -1;
|
||||
}
|
||||
|
||||
args.Logger?.ILog("File successfully uploaded");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
5
Nextcloud/GlobalUsings.cs
Normal file
5
Nextcloud/GlobalUsings.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
global using System;
|
||||
global using System.Text;
|
||||
global using System.ComponentModel.DataAnnotations;
|
||||
global using FileFlows.Plugin;
|
||||
global using FileFlows.Plugin.Attributes;
|
||||
167
Nextcloud/Helpers/NextcloudUploader.cs
Normal file
167
Nextcloud/Helpers/NextcloudUploader.cs
Normal file
@@ -0,0 +1,167 @@
|
||||
using System.Net;
|
||||
|
||||
namespace FileFlows.Nextcloud.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for uploading files to Nextcloud
|
||||
/// </summary>
|
||||
/// <param name="logger">the Logger to use</param>
|
||||
/// <param name="nextcloudUrl">The URL of the Nextcloud instance.</param>
|
||||
/// <param name="username">The username for authentication.</param>
|
||||
/// <param name="password">The password for authentication.</param>
|
||||
public class NextcloudUploader(ILogger logger, string nextcloudUrl, string username, string password)
|
||||
{
|
||||
private static HttpClient client;
|
||||
|
||||
static NextcloudUploader()
|
||||
{
|
||||
var handler = new HttpClientHandler
|
||||
{
|
||||
// Customize the handler as needed
|
||||
ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true // Ignore certificate errors
|
||||
};
|
||||
client = new HttpClient( handler );
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Uploads a file to Nextcloud
|
||||
/// </summary>
|
||||
/// <param name="localFilePath">The full path to the file on disk to be uploaded.</param>
|
||||
/// <param name="remoteFilePath">The path in Nextcloud where the file should be uploaded.</param>
|
||||
/// <returns>True if the file was uploaded successfully; otherwise, false.</returns>
|
||||
public Result<bool> UploadFile(string localFilePath, string remoteFilePath)
|
||||
{
|
||||
logger?.ILog("Uploading file: " + localFilePath);
|
||||
try
|
||||
{
|
||||
string remoteFolder = remoteFilePath.Replace("\\", "/");
|
||||
remoteFolder = string.Join("/", remoteFolder.Split("/")[..^1]);
|
||||
logger?.ILog("Remote Folder: " + remoteFolder);
|
||||
|
||||
var fileStream = File.OpenRead(localFilePath);
|
||||
|
||||
int chunkIndex = 1;
|
||||
int chunkSize = 80_000_000; // Split into chunks of approximately 80 MB
|
||||
|
||||
// Calculate the number of chunks based on the size of the compressed stream
|
||||
long fileSize = fileStream.Length;
|
||||
int numberOfChunks = (int)Math.Ceiling((double)fileSize / chunkSize);
|
||||
|
||||
fileStream.Position = 0; // Reset stream position
|
||||
|
||||
// Upload each chunk
|
||||
while (fileStream.Position < fileSize)
|
||||
{
|
||||
int bytesToRead = (int)Math.Min(chunkSize, fileSize - fileStream.Position);
|
||||
byte[] buffer = new byte[bytesToRead];
|
||||
fileStream.Read(buffer, 0, bytesToRead);
|
||||
|
||||
var chunkStream = new MemoryStream(buffer);
|
||||
|
||||
if (chunkIndex == 1)
|
||||
CreateFolder(remoteFolder).Wait();
|
||||
if (numberOfChunks == 1)
|
||||
{
|
||||
// Upload the chunk here using UploadFilePart method
|
||||
UploadFilePart(chunkStream, remoteFilePath).Wait();
|
||||
}
|
||||
else
|
||||
{
|
||||
UploadFilePart(chunkStream, remoteFilePath).Wait();
|
||||
}
|
||||
chunkStream.Dispose();
|
||||
|
||||
chunkIndex++;
|
||||
}
|
||||
|
||||
logger?.ILog("Upload completed with chunks: " + numberOfChunks);
|
||||
return true; // All files uploaded successfully
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<bool>.Fail($"Error uploading file: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uploads a file to Nextcloud
|
||||
/// </summary>
|
||||
/// <param name="compressedStream">The stream to upload.</param>
|
||||
/// <param name="remoteFilePath">The path in Nextcloud where the file should be uploaded.</param>
|
||||
/// <returns>True if the file was uploaded successfully; otherwise, false.</returns>
|
||||
private async Task<bool> UploadFilePart(MemoryStream compressedStream, string remoteFilePath)
|
||||
{
|
||||
logger?.ILog("Uploading file: " + remoteFilePath);
|
||||
|
||||
try
|
||||
{
|
||||
// Create the WebDAV request URL
|
||||
string url = $"{nextcloudUrl.TrimEnd('/')}/remote.php/dav/files/{username}/{remoteFilePath.TrimStart('/')}";
|
||||
|
||||
// Set the credentials
|
||||
var byteArray = new UTF8Encoding().GetBytes($"{username}:{password}");
|
||||
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));
|
||||
|
||||
// Set the content
|
||||
compressedStream.Position = 0;
|
||||
using (StreamContent content = new StreamContent(compressedStream))
|
||||
{
|
||||
content.Headers.ContentLength = compressedStream.Length;
|
||||
|
||||
// Send the PUT request
|
||||
HttpResponseMessage response = await client.PutAsync(url, content);
|
||||
|
||||
// Check for success
|
||||
if (response.StatusCode == HttpStatusCode.Created)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle any errors
|
||||
logger?.ELog($"Error uploading file: {ex.Message}");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a folder on the WebDAV server at the specified path.
|
||||
/// </summary>
|
||||
/// <param name="remoteFolderPath">The path of the folder to be created on the server.</param>
|
||||
/// <returns>True if the folder creation is successful, otherwise false.</returns>
|
||||
private async Task<bool> CreateFolder(string remoteFolderPath)
|
||||
{
|
||||
logger?.ILog("Creating folder: " + remoteFolderPath);
|
||||
try
|
||||
{
|
||||
// Create the WebDAV request URL
|
||||
string url = $"{nextcloudUrl.TrimEnd('/')}/remote.php/dav/files/{username}/{remoteFolderPath.TrimStart('/')}";
|
||||
|
||||
// Set the credentials
|
||||
var byteArray = new UTF8Encoding().GetBytes($"{username}:{password}");
|
||||
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));
|
||||
|
||||
// Create the MKCOL request
|
||||
HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("MKCOL"), url);
|
||||
|
||||
// Send the request
|
||||
HttpResponseMessage response = await client.SendAsync(request);
|
||||
|
||||
// Check for success
|
||||
if (response.StatusCode == HttpStatusCode.Created)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle any errors
|
||||
logger?.ELog($"Error creating folder: {ex.Message}");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
37
Nextcloud/Nextcloud.csproj
Normal file
37
Nextcloud/Nextcloud.csproj
Normal file
@@ -0,0 +1,37 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>FileFlows.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
|
||||
<FileVersion>1.1.1.528</FileVersion>
|
||||
<ProductVersion>1.1.1.528</ProductVersion>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<Company>FileFlows</Company>
|
||||
<Authors>John Andrews</Authors>
|
||||
<Product>Nextcloud</Product>
|
||||
<PackageProjectUrl>https://fileflows.com/</PackageProjectUrl>
|
||||
<Description>Plugin that provides iteration with Nextcloud</Description>
|
||||
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Update="i18n\*.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="i18n\en.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug'">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.4.3" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.4.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="FileFlows.Plugin">
|
||||
<HintPath>..\FileFlows.Plugin.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
18
Nextcloud/Plugin.cs
Normal file
18
Nextcloud/Plugin.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace FileFlows.Nextcloud;
|
||||
|
||||
public class Plugin : FileFlows.Plugin.IPlugin
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Guid Uid => new Guid("162b4ed0-da61-42de-85e1-576b9d7a2f82");
|
||||
/// <inheritdoc />
|
||||
public string Name => "Nextcloud";
|
||||
/// <inheritdoc />
|
||||
public string MinimumVersion => "24.8.1.3444";
|
||||
/// <inheritdoc />
|
||||
public string Icon => "svg:nextcloud";
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Init()
|
||||
{
|
||||
}
|
||||
}
|
||||
28
Nextcloud/PluginSettings.cs
Normal file
28
Nextcloud/PluginSettings.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace FileFlows.Nextcloud;
|
||||
|
||||
/// <summary>
|
||||
/// The plugin settings for this plugin
|
||||
/// </summary>
|
||||
public class PluginSettings : IPluginSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the next cloud URL
|
||||
/// </summary>
|
||||
[Text(1)]
|
||||
[Required]
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the username
|
||||
/// </summary>
|
||||
[Text(2)]
|
||||
[Required]
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password
|
||||
/// </summary>
|
||||
[Text(3)]
|
||||
[Required]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
25
Nextcloud/Tests/UploadTest.cs
Normal file
25
Nextcloud/Tests/UploadTest.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
#if(DEBUG)
|
||||
|
||||
using FileFlows.Nextcloud.FlowElements;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace FileFlows.Nextcloud.Tests;
|
||||
|
||||
[TestClass]
|
||||
public class UploadTest : TestBase
|
||||
{
|
||||
[TestMethod]
|
||||
public void Test()
|
||||
{
|
||||
var args = new NodeParameters("/home/john/src/ff-files/test-files/images/heic1.heic", Logger, false, string.Empty, new LocalFileService());
|
||||
args.GetPluginSettingsJson = (string input) =>
|
||||
{
|
||||
return File.ReadAllText("../../../../../nextcloud.json");
|
||||
};
|
||||
|
||||
var ele = new UploadToNextcloud();
|
||||
ele.DestinationPath = "ff-test/" + Guid.NewGuid() + ".heic";
|
||||
Assert.AreEqual(1, ele.Execute(args));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
485
Nextcloud/Tests/_LocalFileService.cs
Normal file
485
Nextcloud/Tests/_LocalFileService.cs
Normal file
@@ -0,0 +1,485 @@
|
||||
#if(DEBUG)
|
||||
using FileFlows.Plugin;
|
||||
using FileFlows.Plugin.Models;
|
||||
using FileFlows.Plugin.Services;
|
||||
using System.IO;
|
||||
using FileHelper = FileFlows.Plugin.Helpers.FileHelper;
|
||||
|
||||
namespace FileFlows.Nextcloud.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Local file service
|
||||
/// </summary>
|
||||
public class LocalFileService : IFileService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the path separator for the file system
|
||||
/// </summary>
|
||||
public char PathSeparator { get; init; } = Path.DirectorySeparatorChar;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the allowed paths the file service can access
|
||||
/// </summary>
|
||||
public string[] AllowedPaths { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a function for replacing variables in a string.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The function takes a string input, a boolean indicating whether to strip missing variables,
|
||||
/// and a boolean indicating whether to clean special characters.
|
||||
/// </remarks>
|
||||
#pragma warning disable CS8767 // Nullability of reference types in type of parameter doesn't match implicitly implemented member (possibly because of nullability attributes).
|
||||
public ReplaceVariablesDelegate ReplaceVariables { get; set; } = null!;
|
||||
#pragma warning restore CS8767 // Nullability of reference types in type of parameter doesn't match implicitly implemented member (possibly because of nullability attributes).
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the permissions to use for files
|
||||
/// </summary>
|
||||
public int? Permissions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the owner:group to use for files
|
||||
/// </summary>
|
||||
public string OwnerGroup { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the logger used for logging
|
||||
/// </summary>
|
||||
public ILogger? Logger { get; set; }
|
||||
|
||||
public Result<string[]> GetFiles(string path, string searchPattern = "", bool recursive = false)
|
||||
{
|
||||
if (IsProtectedPath(ref path))
|
||||
return Result<string[]>.Fail("Cannot access protected path: " + path);
|
||||
try
|
||||
{
|
||||
return Directory.GetFiles(path, searchPattern ?? string.Empty,
|
||||
recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return new string[] { };
|
||||
}
|
||||
}
|
||||
|
||||
public Result<string[]> GetDirectories(string path)
|
||||
{
|
||||
if (IsProtectedPath(ref path))
|
||||
return Result<string[]>.Fail("Cannot access protected path: " + path);
|
||||
try
|
||||
{
|
||||
return Directory.GetDirectories(path);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return new string[] { };
|
||||
}
|
||||
}
|
||||
|
||||
public Result<bool> DirectoryExists(string path)
|
||||
{
|
||||
if (IsProtectedPath(ref path))
|
||||
return Result<bool>.Fail("Cannot access protected path: " + path);
|
||||
try
|
||||
{
|
||||
return Directory.Exists(path);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Result<bool> DirectoryDelete(string path, bool recursive = false)
|
||||
{
|
||||
if (IsProtectedPath(ref path))
|
||||
return Result<bool>.Fail("Cannot access protected path: " + path);
|
||||
try
|
||||
{
|
||||
Directory.Delete(path, recursive);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<bool>.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public Result<bool> DirectoryMove(string path, string destination)
|
||||
{
|
||||
if (IsProtectedPath(ref path))
|
||||
return Result<bool>.Fail("Cannot access protected path: " + path);
|
||||
if (IsProtectedPath(ref destination))
|
||||
return Result<bool>.Fail("Cannot access protected path: " + destination);
|
||||
try
|
||||
{
|
||||
Directory.Move(path, destination);
|
||||
SetPermissions(destination);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<bool>.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public Result<bool> DirectoryCreate(string path)
|
||||
{
|
||||
if (IsProtectedPath(ref path))
|
||||
return Result<bool>.Fail("Cannot access protected path: " + path);
|
||||
try
|
||||
{
|
||||
var dirInfo = new DirectoryInfo(path);
|
||||
if (dirInfo.Exists == false)
|
||||
dirInfo.Create();
|
||||
SetPermissions(path);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<bool>.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public Result<DateTime> DirectoryCreationTimeUtc(string path)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Result<DateTime> DirectoryLastWriteTimeUtc(string path)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Result<bool> FileExists(string path)
|
||||
{
|
||||
if (IsProtectedPath(ref path))
|
||||
return Result<bool>.Fail("Cannot access protected path: " + path);
|
||||
try
|
||||
{
|
||||
return System.IO.File.Exists(path);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Result<FileInformation> FileInfo(string path)
|
||||
{
|
||||
if (IsProtectedPath(ref path))
|
||||
return Result<FileInformation>.Fail("Cannot access protected path: " + path);
|
||||
try
|
||||
{
|
||||
FileInfo fileInfo = new FileInfo(path);
|
||||
|
||||
return new FileInformation
|
||||
{
|
||||
CreationTime = fileInfo.CreationTime,
|
||||
CreationTimeUtc = fileInfo.CreationTimeUtc,
|
||||
LastWriteTime = fileInfo.LastWriteTime,
|
||||
LastWriteTimeUtc = fileInfo.LastWriteTimeUtc,
|
||||
Extension = fileInfo.Extension.TrimStart('.'),
|
||||
Name = fileInfo.Name,
|
||||
FullName = fileInfo.FullName,
|
||||
Length = fileInfo.Length,
|
||||
Directory = fileInfo.DirectoryName!
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<FileInformation>.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public Result<bool> FileDelete(string path)
|
||||
{
|
||||
if (IsProtectedPath(ref path))
|
||||
return Result<bool>.Fail("Cannot access protected path: " + path);
|
||||
try
|
||||
{
|
||||
var fileInfo = new FileInfo(path);
|
||||
if(fileInfo.Exists)
|
||||
fileInfo.Delete();
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Result<long> FileSize(string path)
|
||||
{
|
||||
if (IsProtectedPath(ref path))
|
||||
return Result<long>.Fail("Cannot access protected path: " + path);
|
||||
try
|
||||
{
|
||||
var fileInfo = new FileInfo(path);
|
||||
if (fileInfo.Exists == false)
|
||||
return Result<long>.Fail("File does not exist");
|
||||
return fileInfo.Length;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<long>.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public Result<DateTime> FileCreationTimeUtc(string path)
|
||||
{
|
||||
if (IsProtectedPath(ref path))
|
||||
return Result<DateTime>.Fail("Cannot access protected path: " + path);
|
||||
try
|
||||
{
|
||||
var fileInfo = new FileInfo(path);
|
||||
if (fileInfo.Exists == false)
|
||||
return Result<DateTime>.Fail("File does not exist");
|
||||
return fileInfo.CreationTimeUtc;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<DateTime>.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public Result<DateTime> FileLastWriteTimeUtc(string path)
|
||||
{
|
||||
if (IsProtectedPath(ref path))
|
||||
return Result<DateTime>.Fail("Cannot access protected path: " + path);
|
||||
try
|
||||
{
|
||||
var fileInfo = new FileInfo(path);
|
||||
if (fileInfo.Exists == false)
|
||||
return Result<DateTime>.Fail("File does not exist");
|
||||
return fileInfo.LastWriteTimeUtc;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<DateTime>.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public Result<bool> FileMove(string path, string destination, bool overwrite = true)
|
||||
{
|
||||
if (IsProtectedPath(ref path))
|
||||
return Result<bool>.Fail("Cannot access protected path: " + path);
|
||||
if (IsProtectedPath(ref destination))
|
||||
return Result<bool>.Fail("Cannot access protected path: " + destination);
|
||||
try
|
||||
{
|
||||
var fileInfo = new FileInfo(path);
|
||||
if (fileInfo.Exists == false)
|
||||
return Result<bool>.Fail("File does not exist");
|
||||
var destDir = new FileInfo(destination).Directory;
|
||||
if (destDir!.Exists == false)
|
||||
{
|
||||
destDir.Create();
|
||||
SetPermissions(destDir.FullName);
|
||||
}
|
||||
|
||||
fileInfo.MoveTo(destination, overwrite);
|
||||
SetPermissions(destination);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<bool>.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public Result<bool> FileCopy(string path, string destination, bool overwrite = true)
|
||||
{
|
||||
if (IsProtectedPath(ref path))
|
||||
return Result<bool>.Fail("Cannot access protected path: " + path);
|
||||
if (IsProtectedPath(ref destination))
|
||||
return Result<bool>.Fail("Cannot access protected path: " + destination);
|
||||
try
|
||||
{
|
||||
var fileInfo = new FileInfo(path);
|
||||
if (fileInfo.Exists == false)
|
||||
return Result<bool>.Fail("File does not exist");
|
||||
|
||||
var destDir = new FileInfo(destination).Directory;
|
||||
if (destDir!.Exists == false)
|
||||
{
|
||||
destDir.Create();
|
||||
SetPermissions(destDir.FullName);
|
||||
}
|
||||
|
||||
fileInfo.CopyTo(destination, overwrite);
|
||||
SetPermissions(destination);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<bool>.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public Result<bool> FileAppendAllText(string path, string text)
|
||||
{
|
||||
if (IsProtectedPath(ref path))
|
||||
return Result<bool>.Fail("Cannot access protected path: " + path);
|
||||
try
|
||||
{
|
||||
System.IO.File.AppendAllText(path, text);
|
||||
SetPermissions(path);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<bool>.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public bool FileIsLocal(string path) => true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the local path
|
||||
/// </summary>
|
||||
/// <param name="path">the path</param>
|
||||
/// <returns>the local path to the file</returns>
|
||||
public Result<string> GetLocalPath(string path)
|
||||
=> Result<string>.Success(path);
|
||||
|
||||
public Result<bool> Touch(string path)
|
||||
{
|
||||
if (IsProtectedPath(ref path))
|
||||
return Result<bool>.Fail("Cannot access protected path: " + path);
|
||||
|
||||
if (DirectoryExists(path).Is(true))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.SetLastWriteTimeUtc(path, DateTime.UtcNow);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<bool>.Fail("Failed to touch directory: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (System.IO.File.Exists(path))
|
||||
System.IO.File.SetLastWriteTimeUtc(path, DateTime.UtcNow);
|
||||
else
|
||||
{
|
||||
System.IO.File.Create(path);
|
||||
SetPermissions(path);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<bool>.Fail($"Failed to touch file: '{path}' => {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public Result<long> DirectorySize(string path)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Result<bool> SetCreationTimeUtc(string path, DateTime date)
|
||||
{
|
||||
if (IsProtectedPath(ref path))
|
||||
return Result<bool>.Fail("Cannot access protected path: " + path);
|
||||
try
|
||||
{
|
||||
if (!System.IO.File.Exists(path))
|
||||
return Result<bool>.Fail("File not found.");
|
||||
|
||||
System.IO.File.SetCreationTimeUtc(path, date);
|
||||
return Result<bool>.Success(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<bool>.Fail($"Error setting creation time: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public Result<bool> SetLastWriteTimeUtc(string path, DateTime date)
|
||||
{
|
||||
if (IsProtectedPath(ref path))
|
||||
return Result<bool>.Fail("Cannot access protected path: " + path);
|
||||
try
|
||||
{
|
||||
if (!System.IO.File.Exists(path))
|
||||
return Result<bool>.Fail("File not found.");
|
||||
|
||||
System.IO.File.SetLastWriteTimeUtc(path, date);
|
||||
return Result<bool>.Success(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<bool>.Fail($"Error setting last write time: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a path is accessible by the file server
|
||||
/// </summary>
|
||||
/// <param name="path">the path to check</param>
|
||||
/// <returns>true if accessible, otherwise false</returns>
|
||||
private bool IsProtectedPath(ref string path)
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
path = path.Replace("/", "\\");
|
||||
else
|
||||
path = path.Replace("\\", "/");
|
||||
|
||||
if(ReplaceVariables != null)
|
||||
path = ReplaceVariables(path, true);
|
||||
|
||||
if (FileHelper.IsSystemDirectory(path))
|
||||
return true; // a system directory, no access
|
||||
|
||||
if (AllowedPaths?.Any() != true)
|
||||
return false; // no allowed paths configured, allow all
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
path = path.ToLowerInvariant();
|
||||
|
||||
for(int i=0;i<AllowedPaths.Length;i++)
|
||||
{
|
||||
string p = OperatingSystem.IsWindows() ? AllowedPaths[i].ToLowerInvariant().TrimEnd('\\') : AllowedPaths[i].TrimEnd('/');
|
||||
if (path.StartsWith(p))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SetPermissions(string path, int? permissions = null, Action<string>? logMethod = null)
|
||||
{
|
||||
logMethod ??= (string message) => Logger?.ILog(message);
|
||||
|
||||
permissions = permissions != null && permissions > 0 ? permissions : Permissions;
|
||||
if (permissions == null || permissions < 1)
|
||||
permissions = 777;
|
||||
|
||||
|
||||
if ((System.IO.File.Exists(path) == false && Directory.Exists(path) == false))
|
||||
{
|
||||
logMethod("SetPermissions: File doesnt existing, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
//StringLogger stringLogger = new StringLogger();
|
||||
var logger = new TestLogger();
|
||||
|
||||
bool isFile = new FileInfo(path).Exists;
|
||||
|
||||
FileHelper.SetPermissions(logger, path, file: isFile, permissions: permissions);
|
||||
|
||||
FileHelper.ChangeOwner(logger, path, file: isFile, ownerGroup: OwnerGroup);
|
||||
|
||||
logMethod(logger.ToString());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
64
Nextcloud/Tests/_TestBase.cs
Normal file
64
Nextcloud/Tests/_TestBase.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
#if(DEBUG)
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace FileFlows.Nextcloud.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for the tests
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public abstract class TestBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The test context instance
|
||||
/// </summary>
|
||||
private TestContext testContextInstance = null!;
|
||||
|
||||
internal TestLogger Logger = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the test context
|
||||
/// </summary>
|
||||
public TestContext TestContext
|
||||
{
|
||||
get => testContextInstance;
|
||||
set => testContextInstance = value;
|
||||
}
|
||||
|
||||
public string TestPath { get; private set; } = null!;
|
||||
public string TempPath { get; private set; } = null!;
|
||||
|
||||
public readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
public readonly bool IsLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
|
||||
|
||||
[TestInitialize]
|
||||
public void TestInitialize()
|
||||
{
|
||||
Logger.Writer = (msg) => TestContext.WriteLine(msg);
|
||||
|
||||
this.TestPath = this.TestPath?.EmptyAsNull() ?? (IsLinux ? "~/src/ff-files/test-files/videos" : @"d:\videos\testfiles");
|
||||
this.TempPath = this.TempPath?.EmptyAsNull() ?? (IsLinux ? "~/src/ff-files/temp" : @"d:\videos\temp");
|
||||
|
||||
this.TestPath = this.TestPath.Replace("~/", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/");
|
||||
this.TempPath = this.TempPath.Replace("~/", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/");
|
||||
|
||||
if (Directory.Exists(this.TempPath) == false)
|
||||
Directory.CreateDirectory(this.TempPath);
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
public void CleanUp()
|
||||
{
|
||||
TestContext.WriteLine(Logger.ToString());
|
||||
}
|
||||
|
||||
protected virtual void TestStarting()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
90
Nextcloud/Tests/_TestLogger.cs
Normal file
90
Nextcloud/Tests/_TestLogger.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
#if (DEBUG)
|
||||
|
||||
namespace FileFlows.Nextcloud.Tests;
|
||||
|
||||
using FileFlows.Plugin;
|
||||
|
||||
/// <summary>
|
||||
/// A logger for tests that stores the logs in memory
|
||||
/// </summary>
|
||||
public class TestLogger : ILogger
|
||||
{
|
||||
private readonly List<string> Messages = new();
|
||||
|
||||
/// <summary>
|
||||
/// Writes an information log message
|
||||
/// </summary>
|
||||
/// <param name="args">the log parameters</param>
|
||||
public void ILog(params object[] args)
|
||||
=> Log(LogType.Info, args);
|
||||
|
||||
/// <summary>
|
||||
/// Writes an debug log message
|
||||
/// </summary>
|
||||
/// <param name="args">the log parameters</param>
|
||||
public void DLog(params object[] args)
|
||||
=> Log(LogType.Debug, args);
|
||||
|
||||
/// <summary>
|
||||
/// Writes an warning log message
|
||||
/// </summary>
|
||||
/// <param name="args">the log parameters</param>
|
||||
public void WLog(params object[] args)
|
||||
=> Log(LogType.Warning, args);
|
||||
|
||||
/// <summary>
|
||||
/// Writes an error log message
|
||||
/// </summary>
|
||||
/// <param name="args">the log parameters</param>
|
||||
public void ELog(params object[] args)
|
||||
=> Log(LogType.Error, args);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tail of the log
|
||||
/// </summary>
|
||||
/// <param name="length">the number of messages to get</param>
|
||||
public string GetTail(int length = 50)
|
||||
{
|
||||
if (Messages.Count <= length)
|
||||
return string.Join(Environment.NewLine, Messages);
|
||||
return string.Join(Environment.NewLine, Messages.TakeLast(50));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a message
|
||||
/// </summary>
|
||||
/// <param name="type">the type of log to record</param>
|
||||
/// <param name="args">the arguments of the message</param>
|
||||
private void Log(LogType type, params object[] args)
|
||||
{
|
||||
string message = type + " -> " + string.Join(", ", args.Select(x =>
|
||||
x == null ? "null" :
|
||||
x.GetType().IsPrimitive ? x.ToString() :
|
||||
x is string ? x.ToString() :
|
||||
System.Text.Json.JsonSerializer.Serialize(x)));
|
||||
Writer?.Invoke(message);
|
||||
Messages.Add(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional writer
|
||||
/// </summary>
|
||||
public Action<string> Writer { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the entire log as a string
|
||||
/// </summary>
|
||||
/// <returns>the entire log</returns>
|
||||
public override string ToString()
|
||||
=> string.Join(Environment.NewLine, Messages);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the log contains the text
|
||||
/// </summary>
|
||||
/// <param name="text">the text to check for</param>
|
||||
/// <returns>true if it contains it, otherwise false</returns>
|
||||
public bool Contains(string text)
|
||||
=> Messages.Any(x => x.Contains(text));
|
||||
}
|
||||
|
||||
#endif
|
||||
19
Nextcloud/i18n/en.json
Normal file
19
Nextcloud/i18n/en.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"Flow": {
|
||||
"Parts": {
|
||||
"UploadToNextcloud": {
|
||||
"Outputs": {
|
||||
"1": "Was successfully uploaded to Nextcloud",
|
||||
"2": "Failed ot upload to Nextcloud"
|
||||
},
|
||||
"Description": "Uploads a file to Nextcloud",
|
||||
"Fields": {
|
||||
"File": "File",
|
||||
"Url-Help": "The file to upload, leave empty to use the current working file.",
|
||||
"DestinationPath": "Destination",
|
||||
"DestinationPath-Help": "The location and filename, i.e. the full path, of the file in Nextcloud."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user