[merge] Merge branch 'dev-live'

This commit is contained in:
acite
2025-09-09 22:17:02 +08:00
7 changed files with 293 additions and 36 deletions

View File

@@ -10,8 +10,13 @@
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="bf317275-3039-49bb-a475-725a800a0cce" name="Changes" comment=""> <list default="true" id="bf317275-3039-49bb-a475-725a800a0cce" name="Changes" comment="">
<change afterPath="$PROJECT_DIR$/Abyss/Components/Controllers/Media/LiveController.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/.idea.Abyss/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.Abyss/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/.idea.Abyss/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.Abyss/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Abyss/Toolkits/update-video.py" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Toolkits/update-video.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/Abyss/Abyss.csproj" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Abyss.csproj" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Abyss/Components/Controllers/Security/UserController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Controllers/Security/UserController.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Abyss/Components/Services/ResourceService.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Services/ResourceService.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Abyss/Components/Services/UserService.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Services/UserService.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Abyss/Components/Static/Helpers.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Static/Helpers.cs" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -32,10 +37,15 @@
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/7598e47d5cdf4107ba88f8220720fdc89000/a6/79d67871/xxHash128.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/7598e47d5cdf4107ba88f8220720fdc89000/a6/79d67871/xxHash128.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/f09ccaeb94c34c2299acd3efee0facee1a400/81/137b58b4/Key.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/f09ccaeb94c34c2299acd3efee0facee1a400/81/137b58b4/Key.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/AbyssController.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/AbyssController.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/Media/LiveController.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/Security/UserController.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/Security/UserController.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/Task/TaskController.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/Task/TaskController.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="mock:///home/acite/embd/WebProjects/Abyss/Abyss/Components/Services/ConfigureService.cs" root0="SKIP_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/ConfigureService.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$PROJECT_DIR$/Abyss/Components/Services/ConfigureService.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="mock:///home/acite/embd/WebProjects/Abyss/Abyss/Components/Services/ConfigureService.cs" root0="SKIP_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/ResourceService.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$PROJECT_DIR$/Abyss/Components/Services/ResourceService.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="mock:///home/acite/embd/WebProjects/Abyss/Abyss/Components/Services/ResourceService.cs" root0="SKIP_HIGHLIGHTING" />
<setting file="mock:///home/acite/embd/WebProjects/Abyss/Abyss/Components/Services/ResourceService.cs" root0="SKIP_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/TaskService.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$PROJECT_DIR$/Abyss/Components/Services/TaskService.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/UserService.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$PROJECT_DIR$/Abyss/Components/Services/UserService.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Abyss/Components/Static/Helpers.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$PROJECT_DIR$/Abyss/Components/Static/Helpers.cs" root0="FORCE_HIGHLIGHTING" />
@@ -51,7 +61,6 @@
<setting file="file://$PROJECT_DIR$/Abyss/Model/UserCreating.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$PROJECT_DIR$/Abyss/Model/UserCreating.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Abyss/Model/Video.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$PROJECT_DIR$/Abyss/Model/Video.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/AbyssCli/Program.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$PROJECT_DIR$/AbyssCli/Program.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file:///storage/Images/31/summary.json" root0="FORCE_HIGHLIGHTING" />
<setting file="file:///usr/lib/dotnet/sdk/9.0.109/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.Sdk.targets" root0="FORCE_HIGHLIGHTING" /> <setting file="file:///usr/lib/dotnet/sdk/9.0.109/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.Sdk.targets" root0="FORCE_HIGHLIGHTING" />
</component> </component>
<component name="MetaFilesCheckinStateConfiguration" checkMetaFiles="true" /> <component name="MetaFilesCheckinStateConfiguration" checkMetaFiles="true" />
@@ -79,7 +88,7 @@
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true", "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
"RunOnceActivity.git.unshallow": "true", "RunOnceActivity.git.unshallow": "true",
"XThreadsFramesViewSplitterKey": "0.30266345", "XThreadsFramesViewSplitterKey": "0.30266345",
"git-widget-placeholder": "main", "git-widget-placeholder": "dev-live",
"last_opened_file_path": "/storage/Images/31/summary.json", "last_opened_file_path": "/storage/Images/31/summary.json",
"node.js.detected.package.eslint": "true", "node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true", "node.js.detected.package.tslint": "true",

View File

@@ -17,8 +17,4 @@
<PackageReference Include="System.IO.Hashing" Version="10.0.0-preview.7.25380.108" /> <PackageReference Include="System.IO.Hashing" Version="10.0.0-preview.7.25380.108" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Components\Controllers\Media\" />
</ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,66 @@
using Abyss.Components.Services;
using Abyss.Components.Static;
using Microsoft.AspNetCore.Mvc;
namespace Abyss.Components.Controllers.Media;
[ApiController]
[Route("api/[controller]")]
public class LiveController(ILogger<LiveController> logger, ResourceService rs, ConfigureService config): Controller
{
public readonly string LiveFolder = Path.Combine(config.MediaRoot, "Live");
[HttpPost("{id}")]
public async Task<IActionResult> AddLive(string id, string token, string owner)
{
var d = Helpers.SafePathCombine(LiveFolder, [id]);
if (d == null) return StatusCode(403, new { message = "403 Denied" });
bool r = await rs.Include(d, token, Ip, owner, "rw,--,--");
return r ? Ok("Success") : BadRequest();
}
[HttpDelete("{id}")]
public async Task<IActionResult> RemoveLive(string id, string token)
{
var d = Helpers.SafePathCombine(LiveFolder, [id]);
if (d == null)
return StatusCode(403, new { message = "403 Denied" });
bool r = await rs.Exclude(d, token, Ip);
return r ? Ok("Success") : BadRequest();
}
[HttpGet("{id}/{item}")]
public async Task<IActionResult> GetLive(string id, string? token, string item)
{
var d = Helpers.SafePathCombine(LiveFolder, [id, item]);
var f = Helpers.SafePathCombine(LiveFolder, [id]);
if (d == null || f == null) return BadRequest();
// TODO: ffplay does not add the m3u8 query parameter in ts requests, so special treatment is given to ts here
// TODO: It should be pointed out that this implementation is not secure and should be modified in subsequent updates
if (d.EndsWith(".ts"))
{
if(System.IO.File.Exists(d))
return PhysicalFile(d, Helpers.GetContentType(d));
else
return NotFound();
}
if(token == null)
return StatusCode(403, new { message = "403 Denied" });
bool r = await rs.Valid(f, token, OperationType.Read, Ip);
if(!r) return StatusCode(403, new { message = "403 Denied" });
if(System.IO.File.Exists(d))
return PhysicalFile(d, Helpers.GetContentType(d));
else
return NotFound();
}
private string Ip => HttpContext.Connection.RemoteIpAddress?.ToString() ?? "127.0.0.1";
}

View File

@@ -96,6 +96,29 @@ public class UserController(UserService user, ILogger<UserController> logger) :
return Ok("Success"); return Ok("Success");
} }
[HttpGet("{user}/open")]
public async Task<IActionResult> Open(string user, [FromQuery] string token, [FromQuery] string? bindIp = null)
{
var caller = _user.Validate(token, Ip);
if (caller == null || caller != "root")
{
return StatusCode(403, new { message = "Access forbidden" });
}
var target = await _user.QueryUser(user);
if (target == null)
{
return StatusCode(404, new { message = "User not found" });
}
var ipToBind = string.IsNullOrWhiteSpace(bindIp) ? Ip : bindIp;
var t = _user.CreateToken(user, ipToBind, TimeSpan.FromHours(1));
_logger.LogInformation("Root created 1h token for {User}, bound to {BindIp}, request from {ReqIp}", user, ipToBind, Ip);
return Ok(new { token = t, user, boundIp = ipToBind });
}
public static bool IsAlphanumeric(string input) public static bool IsAlphanumeric(string input)
{ {
if (string.IsNullOrEmpty(input)) if (string.IsNullOrEmpty(input))

View File

@@ -1,4 +1,3 @@
// ResourceService.cs // ResourceService.cs
using System.Text; using System.Text;
@@ -13,8 +12,8 @@ namespace Abyss.Components.Services;
public enum OperationType public enum OperationType
{ {
Read, // Query, Read Read, // Query, Read
Write, // Write, Delete Write, // Write, Delete
Security // Chown, Chmod Security // Chown, Chmod
} }
@@ -25,8 +24,8 @@ public class ResourceService
private readonly IMemoryCache _cache; private readonly IMemoryCache _cache;
private readonly UserService _user; private readonly UserService _user;
private readonly SQLiteAsyncConnection _database; private readonly SQLiteAsyncConnection _database;
private static readonly Regex PermissionRegex = private static readonly Regex PermissionRegex =
new(@"^([r-][w-]),([r-][w-]),([r-][w-])$", RegexOptions.Compiled); new(@"^([r-][w-]),([r-][w-]),([r-][w-])$", RegexOptions.Compiled);
public ResourceService(ILogger<ResourceService> logger, ConfigureService config, IMemoryCache cache, public ResourceService(ILogger<ResourceService> logger, ConfigureService config, IMemoryCache cache,
@@ -41,9 +40,16 @@ public class ResourceService
_database.CreateTableAsync<ResourceAttribute>().Wait(); _database.CreateTableAsync<ResourceAttribute>().Wait();
var tasksPath = Helpers.SafePathCombine(_config.MediaRoot, "Tasks"); var tasksPath = Helpers.SafePathCombine(_config.MediaRoot, "Tasks");
if(tasksPath != null) if (tasksPath != null)
{
InsertRaRow(tasksPath, "root", "rw,r-,r-", true).Wait(); InsertRaRow(tasksPath, "root", "rw,r-,r-", true).Wait();
}
var livePath = Helpers.SafePathCombine(_config.MediaRoot, "Live");
if (livePath != null)
{
InsertRaRow(livePath, "root", "rw,r-,r-", true).Wait();
}
} }
// Create UID only for resources, without considering advanced hash security such as adding salt // Create UID only for resources, without considering advanced hash security such as adding salt
@@ -59,16 +65,16 @@ public class ResourceService
// Path is abs path here, due to Helpers.SafePathCombine // Path is abs path here, due to Helpers.SafePathCombine
if (!path.StartsWith(Path.GetFullPath(_config.MediaRoot), StringComparison.OrdinalIgnoreCase)) if (!path.StartsWith(Path.GetFullPath(_config.MediaRoot), StringComparison.OrdinalIgnoreCase))
return false; return false;
path = Path.GetRelativePath(_config.MediaRoot, path); path = Path.GetRelativePath(_config.MediaRoot, path);
string? username = _user.Validate(token, ip); string? username = _user.Validate(token, ip);
if (username == null) if (username == null)
{ {
// No permission granted for invalid tokens // No permission granted for invalid tokens
_logger.LogError($"Invalid token: {token}"); _logger.LogError($"Invalid token: {token}");
return false; return false;
} }
User? user = await _user.QueryUser(username); User? user = await _user.QueryUser(username);
if (user == null || user.Name != username) if (user == null || user.Name != username)
@@ -76,7 +82,7 @@ public class ResourceService
_logger.LogError($"Verification failed: {token}"); _logger.LogError($"Verification failed: {token}");
return false; // Two-factor authentication return false; // Two-factor authentication
} }
var parts = path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) var parts = path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
.Where(p => !string.IsNullOrEmpty(p)) .Where(p => !string.IsNullOrEmpty(p))
.ToArray(); .ToArray();
@@ -107,7 +113,7 @@ public class ResourceService
_logger.LogError($"Permission check failed: User: {username}, Resource: {path}, Type: {type.ToString()} "); _logger.LogError($"Permission check failed: User: {username}, Resource: {path}, Type: {type.ToString()} ");
return false; return false;
} }
var l = await CheckPermission(user, ra, type); var l = await CheckPermission(user, ra, type);
if (!l) if (!l)
{ {
@@ -120,8 +126,8 @@ public class ResourceService
private async Task<bool> CheckPermission(User? user, ResourceAttribute? ra, OperationType type) private async Task<bool> CheckPermission(User? user, ResourceAttribute? ra, OperationType type)
{ {
if (user == null || ra == null) return false; if (user == null || ra == null) return false;
if(!PermissionRegex.IsMatch(ra.Permission)) return false; if (!PermissionRegex.IsMatch(ra.Permission)) return false;
var perms = ra.Permission.Split(','); var perms = ra.Permission.Split(',');
if (perms.Length != 3) return false; if (perms.Length != 3) return false;
@@ -154,7 +160,7 @@ public class ResourceService
public async Task<string[]?> Query(string path, string token, string ip) public async Task<string[]?> Query(string path, string token, string ip)
{ {
if(!await Valid(path, token, OperationType.Read, ip)) if (!await Valid(path, token, OperationType.Read, ip))
return null; return null;
if (Helpers.GetPathType(path) != PathType.Directory) if (Helpers.GetPathType(path) != PathType.Directory)
@@ -183,9 +189,11 @@ public class ResourceService
var requester = _user.Validate(token, ip); var requester = _user.Validate(token, ip);
if (requester != "root") if (requester != "root")
{ {
_logger.LogWarning($"Permission denied: Non-root user '{requester ?? "unknown"}' attempted to initialize resources."); _logger.LogWarning(
$"Permission denied: Non-root user '{requester ?? "unknown"}' attempted to initialize resources.");
return false; return false;
} }
debug: debug:
// 2. Validation: Ensure the target path and owner are valid // 2. Validation: Ensure the target path and owner are valid
if (!Directory.Exists(path)) if (!Directory.Exists(path))
@@ -212,8 +220,9 @@ public class ResourceService
{ {
var currentPath = Path.GetRelativePath(_config.MediaRoot, p); var currentPath = Path.GetRelativePath(_config.MediaRoot, p);
var uid = Uid(currentPath); var uid = Uid(currentPath);
var existing = await _database.Table<ResourceAttribute>().Where(r => r.Uid == uid).FirstOrDefaultAsync(); var existing = await _database.Table<ResourceAttribute>().Where(r => r.Uid == uid)
.FirstOrDefaultAsync();
// If it's not in the database, add it to our list for batch insertion // If it's not in the database, add it to our list for batch insertion
if (existing == null) if (existing == null)
{ {
@@ -231,11 +240,13 @@ public class ResourceService
if (newResources.Any()) if (newResources.Any())
{ {
await _database.InsertAllAsync(newResources); await _database.InsertAllAsync(newResources);
_logger.LogInformation($"Successfully initialized {newResources.Count} new resources under '{path}' for user '{username}'."); _logger.LogInformation(
$"Successfully initialized {newResources.Count} new resources under '{path}' for user '{username}'.");
} }
else else
{ {
_logger.LogInformation($"No new resources to initialize under '{path}'. All items already exist in the database."); _logger.LogInformation(
$"No new resources to initialize under '{path}'. All items already exist in the database.");
} }
return true; return true;
@@ -256,12 +267,132 @@ public class ResourceService
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public async Task<bool> Exclude(string path, string token, string ip)
{
var requester = _user.Validate(token, ip);
if (requester != "root")
{
_logger.LogWarning($"Permission denied: Non-root user '{requester ?? "unknown"}' attempted to exclude resource '{path}'.");
return false;
}
try
{
var relPath = Path.GetRelativePath(_config.MediaRoot, path);
var uid = Uid(relPath);
var resource = await _database.Table<ResourceAttribute>().Where(r => r.Uid == uid).FirstOrDefaultAsync();
if (resource == null)
{
_logger.LogError($"Exclude failed: Resource '{relPath}' not found in database.");
return false;
}
var deleted = await _database.DeleteAsync(resource);
if (deleted > 0)
{
_logger.LogInformation($"Successfully excluded resource '{relPath}' from management.");
return true;
}
else
{
_logger.LogError($"Failed to exclude resource '{relPath}' from database.");
return false;
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error excluding resource '{path}'.");
return false;
}
}
public async Task<bool> Include(string path, string token, string ip, string owner, string permission)
{
var requester = _user.Validate(token, ip);
if (requester != "root")
{
_logger.LogWarning(
$"Permission denied: Non-root user '{requester ?? "unknown"}' attempted to include resource '{path}'.");
return false;
}
if (!PermissionRegex.IsMatch(permission))
{
_logger.LogError($"Invalid permission format: {permission}");
return false;
}
var ownerUser = await _user.QueryUser(owner);
if (ownerUser == null)
{
_logger.LogError($"Include failed: Owner user '{owner}' does not exist.");
return false;
}
try
{
var relPath = Path.GetRelativePath(_config.MediaRoot, path);
var uid = Uid(relPath);
var existing = await _database.Table<ResourceAttribute>().Where(r => r.Uid == uid).FirstOrDefaultAsync();
if (existing != null)
{
_logger.LogError($"Include failed: Resource '{relPath}' already exists in database.");
return false;
}
var newResource = new ResourceAttribute
{
Uid = uid,
Name = relPath,
Owner = owner,
Permission = permission
};
var inserted = await _database.InsertAsync(newResource);
if (inserted > 0)
{
_logger.LogInformation(
$"Successfully included '{relPath}' into resource management (Owner={owner}, Permission={permission}).");
return true;
}
else
{
_logger.LogError($"Failed to include resource '{relPath}' into database.");
return false;
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error including resource '{path}'.");
return false;
}
}
public async Task<bool> Exists(string path)
{
try
{
var relPath = Path.GetRelativePath(_config.MediaRoot, path);
var uid = Uid(relPath);
var resource = await _database.Table<ResourceAttribute>().Where(r => r.Uid == uid).FirstOrDefaultAsync();
return resource != null;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error checking existence of resource '{path}'.");
return false;
}
}
public async Task<bool> Chmod(string path, string token, string permission, string ip) public async Task<bool> Chmod(string path, string token, string permission, string ip)
{ {
if(!await Valid(path, token, OperationType.Security, ip)) if (!await Valid(path, token, OperationType.Security, ip))
return false; return false;
// Validate the permission format using the existing regex // Validate the permission format using the existing regex
if (!PermissionRegex.IsMatch(permission)) if (!PermissionRegex.IsMatch(permission))
{ {
@@ -274,7 +405,7 @@ public class ResourceService
path = Path.GetRelativePath(_config.MediaRoot, path); path = Path.GetRelativePath(_config.MediaRoot, path);
var uid = Uid(path); var uid = Uid(path);
var resource = await _database.Table<ResourceAttribute>().Where(r => r.Uid == uid).FirstOrDefaultAsync(); var resource = await _database.Table<ResourceAttribute>().Where(r => r.Uid == uid).FirstOrDefaultAsync();
if (resource == null) if (resource == null)
{ {
_logger.LogError($"Resource not found: {path}"); _logger.LogError($"Resource not found: {path}");
@@ -283,7 +414,7 @@ public class ResourceService
resource.Permission = permission; resource.Permission = permission;
var rowsAffected = await _database.UpdateAsync(resource); var rowsAffected = await _database.UpdateAsync(resource);
if (rowsAffected > 0) if (rowsAffected > 0)
{ {
_logger.LogInformation($"Successfully changed permissions for '{path}' to '{permission}'"); _logger.LogInformation($"Successfully changed permissions for '{path}' to '{permission}'");
@@ -305,9 +436,9 @@ public class ResourceService
public async Task<bool> Chown(string path, string token, string owner, string ip) public async Task<bool> Chown(string path, string token, string owner, string ip)
{ {
if(!await Valid(path, token, OperationType.Security, ip)) if (!await Valid(path, token, OperationType.Security, ip))
return false; return false;
// Validate that the new owner exists // Validate that the new owner exists
var newOwner = await _user.QueryUser(owner); var newOwner = await _user.QueryUser(owner);
if (newOwner == null) if (newOwner == null)
@@ -321,7 +452,7 @@ public class ResourceService
path = Path.GetRelativePath(_config.MediaRoot, path); path = Path.GetRelativePath(_config.MediaRoot, path);
var uid = Uid(path); var uid = Uid(path);
var resource = await _database.Table<ResourceAttribute>().Where(r => r.Uid == uid).FirstOrDefaultAsync(); var resource = await _database.Table<ResourceAttribute>().Where(r => r.Uid == uid).FirstOrDefaultAsync();
if (resource == null) if (resource == null)
{ {
_logger.LogError($"Resource not found: {path}"); _logger.LogError($"Resource not found: {path}");
@@ -330,7 +461,7 @@ public class ResourceService
resource.Owner = owner; resource.Owner = owner;
var rowsAffected = await _database.UpdateAsync(resource); var rowsAffected = await _database.UpdateAsync(resource);
if (rowsAffected > 0) if (rowsAffected > 0)
{ {
_logger.LogInformation($"Successfully changed ownership of '{path}' to '{owner}'"); _logger.LogInformation($"Successfully changed ownership of '{path}' to '{owner}'");
@@ -356,9 +487,9 @@ public class ResourceService
_logger.LogError($"Invalid permission format: {permission}"); _logger.LogError($"Invalid permission format: {permission}");
return false; return false;
} }
var path = Path.GetRelativePath(_config.MediaRoot, fullPath); var path = Path.GetRelativePath(_config.MediaRoot, fullPath);
if (update) if (update)
return await _database.InsertOrReplaceAsync(new ResourceAttribute() return await _database.InsertOrReplaceAsync(new ResourceAttribute()
{ {

View File

@@ -179,4 +179,12 @@ public class UserService
var algorithm = SignatureAlgorithm.Ed25519; var algorithm = SignatureAlgorithm.Ed25519;
return algorithm.Verify(publicKey, data, signature); return algorithm.Verify(publicKey, data, signature);
} }
public string CreateToken(string user, string ip, TimeSpan lifetime)
{
var token = GenerateRandomAsciiString(64);
_cache.Set(token, $"{user}@{ip}", DateTimeOffset.Now.Add(lifetime));
_logger.LogInformation($"Created token for {user}@{ip}, valid {lifetime.TotalMinutes} minutes");
return token;
}
} }

View File

@@ -1,11 +1,35 @@
using System.Globalization; using System.Globalization;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Microsoft.AspNetCore.StaticFiles;
namespace Abyss.Components.Static; namespace Abyss.Components.Static;
public static class Helpers public static class Helpers
{ {
private static readonly FileExtensionContentTypeProvider _provider = InitProvider();
private static FileExtensionContentTypeProvider InitProvider()
{
var provider = new FileExtensionContentTypeProvider();
provider.Mappings[".m3u8"] = "application/vnd.apple.mpegurl";
provider.Mappings[".ts"] = "video/mp2t";
provider.Mappings[".mpd"] = "application/dash+xml";
return provider;
}
public static string GetContentType(string path)
{
if (_provider.TryGetContentType(path, out var contentType))
{
return contentType;
}
return "application/octet-stream";
}
public static string? SafePathCombine(string basePath, params string[] pathParts) public static string? SafePathCombine(string basePath, params string[] pathParts)
{ {
if (string.IsNullOrWhiteSpace(basePath)) if (string.IsNullOrWhiteSpace(basePath))