Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ Implemented as a clean architecture, to replace the FileStorage, replace the [We

## Installation

1. Use docker-compose to start application in docker (http://localhost:5000/swagger)
2. Use [BitKinex client](http://www.bitkinex.com/) for connect with webdav server
1. Navigate to the Database folder in the command-line and add the migrations by executing add-migrations.ps1 with an argument of the name you'd like to give it or
```dotnet ef migrations add postgres --startup-project ./../WebDavServer.WebApi --project ./../WebDavServer.EF.Postgres.FileStorage -c FileStoragePostgresDbContext -o Migrations```
3. Use docker-compose to start application in docker, you can navigate to http://localhost:5000/swagger for the exposed API end-points.
4. Use any WebDAV client you'd like and connect to http://localhost:5000/, alternatively on Windows, you can map a drive and give it that location.
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Microsoft.AspNetCore.StaticFiles;
using System.Text.RegularExpressions;
using WebDavServer.Application.Contracts.Cache;
using WebDavServer.Application.Contracts.FileStorage;
using WebDavServer.Application.Contracts.FileStorage.Enums;
Expand Down Expand Up @@ -354,7 +353,7 @@ private async Task<ErrorType> CreateDirectoryAsync(string path, CancellationToke
{
var pathInfo = await _pathService.GetDestinationPathInfoAsync(path, cancellationToken);

if (!Regex.IsMatch(pathInfo.ResourceName, @"^[a-zA-Z0-9_]+$", RegexOptions.Compiled))
if (HasInvalidChars(pathInfo.ResourceName))
{
return ErrorType.PartResourcePathNotExists;
}
Expand Down Expand Up @@ -447,5 +446,9 @@ private string GetContentType(string fileName)

return "text/plain";
}
private bool HasInvalidChars(string directoryName)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private static

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why it should be static? Im trying to learn clean architecture as I go

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not about clean architecture, but about clean code. Since the method does not use class members, it should be defined as static

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/static#example---static-field-and-method

{
return (!string.IsNullOrEmpty(directoryName) && directoryName.IndexOfAny(Path.GetInvalidPathChars()) >= 0);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove extra parentheses "(" ")"

}
}
}
19 changes: 11 additions & 8 deletions WebDavServer.Infrastructure.FileStorage/Services/PathService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ public async Task<PathInfo> GetDestinationPathInfoAsync(string relativePath, Can

if (directories.Any())
{
var nextDirectory = directories.First();
var nextDirectory = directories[0];
var otherDirectories = directories.Skip(1).ToList();

var directoryInfo = await GetItemAsync(null, string.Empty, nextDirectory, otherDirectories, cancellationToken);

directory = GetLastChild(directoryInfo);
directory = GetLastChild(directoryInfo!);
}

return new PathInfo
Expand All @@ -50,17 +50,20 @@ public async Task<PathInfo> GetDestinationPathInfoAsync(string relativePath, Can

var directories = relativePathTrim.Split("/").Where(x => !string.IsNullOrEmpty(x)).ToList();

var isDirectory = relativePathTrim.EndsWith("/");
var isDirectory = relativePathTrim == "/" || !Path.HasExtension(relativePathTrim);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file name does not necessarily contain extensions (for example, "my_file_name"), you need to check for the presence of a "/" at the end.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All path names, even directories via WebDAV comss through with no "/" at the end of the name, which meant that all directories other than root were seen as files, and took the wrong code-path

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats problem concrete client, by specification required / for collections (folder)

http://www.webdav.org/specs/rfc4918.html#rfc.section.5.2.p.8

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not from what I observed. URL does end with "/" (even files), but path via GetPath doesnt. Might be the way Microsoft implemented WebDAV via the map network drive functionality, which would be strange not to follow RFC (but not unlike Microsoft). Other WebDAV server we are running follows RFC and works with that though. Maybe I am missing something.


directories.Insert(0, RootDirectory);

var resourceName = directories.Last();
directories.RemoveAt(directories.Count - 1);
var resourceName = directories[directories.Count - 1];
if (!isDirectory)
{
directories.RemoveAt(directories.Count - 1);
}

return (resourceName, directories, isDirectory);
}

private async Task<PathInfo> GetItemAsync(
private async Task<PathInfo?> GetItemAsync(
long? parentDirectoryId,
string relativePath,
string directoryName,
Expand All @@ -77,14 +80,14 @@ private async Task<PathInfo> GetItemAsync(

if (item == null)
{
throw new FileStorageException(ErrorCodes.PartOfPathNotExists);
return default;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use http://www.webdav.org/neon/litmus/ for testing all changes, especially this place

}

var virtualPath = $"{relativePath}/{directoryName}";

if (nextDirectories.Any())
{
var nextDirectory = nextDirectories.First();
var nextDirectory = nextDirectories[0];
var otherDirectories = nextDirectories.Skip(1).ToList();

child = await GetItemAsync(item.Id, virtualPath, nextDirectory, otherDirectories, cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,12 @@ public async Task<List<Item>> GetDirectoryInfoAsync(PathInfo pathInfo, bool with
var directory = await _dbContext.Set<Item>()
.Where(x => x.IsDirectory)
.Where(x => x.Title == pathInfo.ResourceName)
.Where(x => x.DirectoryId == directoryId)
.FirstAsync(cancellationToken);
var result = new List<Item> { directory };
.Where(x => x.Id == directoryId)
.FirstOrDefaultAsync(cancellationToken);

var result = directory != null ? new List<Item> { directory } : new List<Item>();

if (withContent)
if (directory is not null && withContent)
{
var contents = await GetDirectoryAsync(directory.Id, cancellationToken);
result.AddRange(contents);
Expand Down Expand Up @@ -140,7 +140,7 @@ public async Task MoveDirectoryAsync(PathInfo srcPath, PathInfo dstPath, Cancell
var item = await _dbContext.Set<Item>()
.Where(x => x.IsDirectory)
.Where(x => x.Title == srcPath.ResourceName)
.Where(x => x.DirectoryId == directoryId)
.Where(x => x.Id == directoryId)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in this filter, the item is searched by the parent directory in which it should be located and the name of the items, and not by its id. If we had an id here, it would be enough to do Find(id)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my observations, in sub-directories, that is not in root, this results in not finding any information from the database. Works fine if all directories will always be in root

.FirstOrDefaultAsync(cancellationToken);

if (item is null)
Expand All @@ -150,6 +150,7 @@ public async Task MoveDirectoryAsync(PathInfo srcPath, PathInfo dstPath, Cancell

item.DirectoryId = dstPath.Directory.Id;
item.Title = dstPath.ResourceName;
item.Name = dstPath.ResourceName;

await _dbContext.SaveChangesAsync(cancellationToken);
}
Expand Down
8 changes: 6 additions & 2 deletions WebDavServer.Infrastructure.WebDav/Services/WebDavService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,12 @@ public async Task<string> PropfindAsync(PropfindRequest r, CancellationToken can

var xMultiStatus = XmlHelper.GetRoot(ns, "multistatus", dictNamespaces);

var xResponse = GetPropfindXmlResponse(ns, new List<string>(), propertiesList.First(), r.Url);
xMultiStatus.Add(xResponse);
XElement xResponse;
if (propertiesList.Count > 0)
{
xResponse = GetPropfindXmlResponse(ns, new List<string>(), propertiesList.First(), r.Url);
xMultiStatus.Add(xResponse);
}

foreach (var properties in propertiesList.Skip(1))
{
Expand Down
13 changes: 11 additions & 2 deletions WebDavServer.WebApi/Controllers/WebDavController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,17 @@ public async Task GetAsync(string? path, CancellationToken cancellationToken = d

[ApiExplorerSettings(IgnoreApi = true)]
[AcceptVerbs("PROPFIND")]
public async Task<string> PropfindAsync(string? path, CancellationToken cancellationToken)
public async Task<IActionResult> PropfindAsync(string? path, CancellationToken cancellationToken)
{
if (path is not null && (path.Contains("desktop.ini") ||
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do these files need to be hidden?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Annoying windows explorer-only stuff that gets checked on a file system that would never contain them *shrug :)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Place the list of files in a separate class, something like this

public static class WebDavConsts {

public static string[] ExcludeHiddenFiles = new[] { ... }
}

...
// use
if (WebDavConsts.ExcludeHiddenFiles.Any(path.Contains)) { ... }

path.Contains("folder.gif") ||
path.Contains("folder.jpg") ||
path.Contains("thumbs.db")))
{

return StatusCode((int)HttpStatusCode.NotFound);
}

var returnXml = await _webDavService.PropfindAsync(new PropfindRequest
{
Url = $"{Request.GetDisplayUrl().TrimEnd('/')}/",
Expand All @@ -59,7 +68,7 @@ public async Task<string> PropfindAsync(string? path, CancellationToken cancella

Response.StatusCode = (int)HttpStatusCode.MultiStatus;

return returnXml;
return Content(returnXml, "application/xml", Encoding.UTF8);
}

[HttpHead]
Expand Down
6 changes: 3 additions & 3 deletions WebDavServer.WebApi/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.

FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
WORKDIR /app
EXPOSE 80

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
ENV ASPNETCORE_ENVIRONMENT=Staging

WORKDIR /src
COPY ["WebDavServer.WebApi/WebDavServer.WebApi.csproj", "WebDavServer.WebApi/"]
COPY ["WebDavServer.Infrastructure/WebDavServer.Infrastructure.csproj", "WebDavServer.Infrastructure/"]
COPY ["WebDavServer.Infrastructure.FileStorage/WebDavServer.Infrastructure.FileStorage.csproj", "WebDavServer.Infrastructure.FileStorage/"]
COPY ["WebDavService.Application/WebDavService.Application.csproj", "WebDavService.Application/"]
COPY ["WebDavServer.Application/WebDavServer.Application.csproj", "WebDavService.Application/"]
COPY ["WebDavServer.Infrastructure.WebDav/WebDavServer.Infrastructure.WebDav.csproj", "WebDavServer.Infrastructure.WebDav/"]
COPY ["WebDavServer.Infrastructure.Cache/WebDavServer.Infrastructure.Cache.csproj", "WebDavServer.Infrastructure.Cache/"]
RUN dotnet restore "WebDavServer.WebApi/WebDavServer.WebApi.csproj"
Expand Down