Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ef20f7dc7 | ||
|
|
5c4f1a87d2 | ||
|
|
c14823f971 |
24
.idea/.idea.Abyss/.idea/workspace.xml
generated
24
.idea/.idea.Abyss/.idea/workspace.xml
generated
@@ -10,21 +10,13 @@
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="bf317275-3039-49bb-a475-725a800a0cce" name="Changes" comment="">
|
||||
<change afterPath="$PROJECT_DIR$/Abyss/Toolkits/image-creator.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/Abyss/Toolkits/image-sum.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/Abyss/Toolkits/update-tags.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/Abyss/Toolkits/update-video.py" afterDir="false" />
|
||||
<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$/Abyss.sln.DotSettings.user" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss.sln.DotSettings.user" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Abyss/Components/Controllers/Media/ImageController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Controllers/Media/ImageController.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Abyss/Components/Controllers/Media/VideoController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Controllers/Media/VideoController.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Abyss/Components/Services/ConfigureService.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Services/ConfigureService.cs" 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/Tools/NaturalStringComparer.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Tools/NaturalStringComparer.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Abyss/Model/Bookmark.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Model/Bookmark.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Abyss/Model/Comic.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Model/Comic.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Abyss/Properties/launchSettings.json" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Properties/launchSettings.json" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Abyss/Components/Static/Helpers.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Static/Helpers.cs" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -45,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/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/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/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="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="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/UserService.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Static/Helpers.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
@@ -64,7 +61,6 @@
|
||||
<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$/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" />
|
||||
</component>
|
||||
<component name="MetaFilesCheckinStateConfiguration" checkMetaFiles="true" />
|
||||
@@ -92,7 +88,7 @@
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"XThreadsFramesViewSplitterKey": "0.30266345",
|
||||
"git-widget-placeholder": "dev-task",
|
||||
"git-widget-placeholder": "dev-live",
|
||||
"last_opened_file_path": "/storage/Images/31/summary.json",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
|
||||
@@ -17,8 +17,4 @@
|
||||
<PackageReference Include="System.IO.Hashing" Version="10.0.0-preview.7.25380.108" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Components\Controllers\Media\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
66
Abyss/Components/Controllers/Media/LiveController.cs
Normal file
66
Abyss/Components/Controllers/Media/LiveController.cs
Normal 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";
|
||||
}
|
||||
@@ -96,6 +96,29 @@ public class UserController(UserService user, ILogger<UserController> logger) :
|
||||
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)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
// ResourceService.cs
|
||||
|
||||
using System.Text;
|
||||
@@ -13,8 +12,8 @@ namespace Abyss.Components.Services;
|
||||
|
||||
public enum OperationType
|
||||
{
|
||||
Read, // Query, Read
|
||||
Write, // Write, Delete
|
||||
Read, // Query, Read
|
||||
Write, // Write, Delete
|
||||
Security // Chown, Chmod
|
||||
}
|
||||
|
||||
@@ -25,8 +24,8 @@ public class ResourceService
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly UserService _user;
|
||||
private readonly SQLiteAsyncConnection _database;
|
||||
|
||||
private static readonly Regex PermissionRegex =
|
||||
|
||||
private static readonly Regex PermissionRegex =
|
||||
new(@"^([r-][w-]),([r-][w-]),([r-][w-])$", RegexOptions.Compiled);
|
||||
|
||||
public ResourceService(ILogger<ResourceService> logger, ConfigureService config, IMemoryCache cache,
|
||||
@@ -41,9 +40,16 @@ public class ResourceService
|
||||
_database.CreateTableAsync<ResourceAttribute>().Wait();
|
||||
|
||||
var tasksPath = Helpers.SafePathCombine(_config.MediaRoot, "Tasks");
|
||||
if(tasksPath != null)
|
||||
if (tasksPath != null)
|
||||
{
|
||||
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
|
||||
@@ -59,16 +65,16 @@ public class ResourceService
|
||||
// Path is abs path here, due to Helpers.SafePathCombine
|
||||
if (!path.StartsWith(Path.GetFullPath(_config.MediaRoot), StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
|
||||
path = Path.GetRelativePath(_config.MediaRoot, path);
|
||||
|
||||
|
||||
string? username = _user.Validate(token, ip);
|
||||
if (username == null)
|
||||
{
|
||||
// No permission granted for invalid tokens
|
||||
_logger.LogError($"Invalid token: {token}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
User? user = await _user.QueryUser(username);
|
||||
if (user == null || user.Name != username)
|
||||
@@ -76,7 +82,7 @@ public class ResourceService
|
||||
_logger.LogError($"Verification failed: {token}");
|
||||
return false; // Two-factor authentication
|
||||
}
|
||||
|
||||
|
||||
var parts = path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
|
||||
.Where(p => !string.IsNullOrEmpty(p))
|
||||
.ToArray();
|
||||
@@ -107,7 +113,7 @@ public class ResourceService
|
||||
_logger.LogError($"Permission check failed: User: {username}, Resource: {path}, Type: {type.ToString()} ");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
var l = await CheckPermission(user, ra, type);
|
||||
if (!l)
|
||||
{
|
||||
@@ -120,8 +126,8 @@ public class ResourceService
|
||||
private async Task<bool> CheckPermission(User? user, ResourceAttribute? ra, OperationType type)
|
||||
{
|
||||
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(',');
|
||||
if (perms.Length != 3) return false;
|
||||
@@ -154,7 +160,7 @@ public class ResourceService
|
||||
|
||||
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;
|
||||
|
||||
if (Helpers.GetPathType(path) != PathType.Directory)
|
||||
@@ -183,9 +189,11 @@ public class ResourceService
|
||||
var requester = _user.Validate(token, ip);
|
||||
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;
|
||||
}
|
||||
|
||||
debug:
|
||||
// 2. Validation: Ensure the target path and owner are valid
|
||||
if (!Directory.Exists(path))
|
||||
@@ -212,8 +220,9 @@ public class ResourceService
|
||||
{
|
||||
var currentPath = Path.GetRelativePath(_config.MediaRoot, p);
|
||||
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 (existing == null)
|
||||
{
|
||||
@@ -231,11 +240,13 @@ public class ResourceService
|
||||
if (newResources.Any())
|
||||
{
|
||||
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
|
||||
{
|
||||
_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;
|
||||
@@ -256,12 +267,132 @@ public class ResourceService
|
||||
{
|
||||
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)
|
||||
{
|
||||
if(!await Valid(path, token, OperationType.Security, ip))
|
||||
if (!await Valid(path, token, OperationType.Security, ip))
|
||||
return false;
|
||||
|
||||
|
||||
// Validate the permission format using the existing regex
|
||||
if (!PermissionRegex.IsMatch(permission))
|
||||
{
|
||||
@@ -274,7 +405,7 @@ public class ResourceService
|
||||
path = Path.GetRelativePath(_config.MediaRoot, path);
|
||||
var uid = Uid(path);
|
||||
var resource = await _database.Table<ResourceAttribute>().Where(r => r.Uid == uid).FirstOrDefaultAsync();
|
||||
|
||||
|
||||
if (resource == null)
|
||||
{
|
||||
_logger.LogError($"Resource not found: {path}");
|
||||
@@ -283,7 +414,7 @@ public class ResourceService
|
||||
|
||||
resource.Permission = permission;
|
||||
var rowsAffected = await _database.UpdateAsync(resource);
|
||||
|
||||
|
||||
if (rowsAffected > 0)
|
||||
{
|
||||
_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)
|
||||
{
|
||||
if(!await Valid(path, token, OperationType.Security, ip))
|
||||
if (!await Valid(path, token, OperationType.Security, ip))
|
||||
return false;
|
||||
|
||||
|
||||
// Validate that the new owner exists
|
||||
var newOwner = await _user.QueryUser(owner);
|
||||
if (newOwner == null)
|
||||
@@ -321,7 +452,7 @@ public class ResourceService
|
||||
path = Path.GetRelativePath(_config.MediaRoot, path);
|
||||
var uid = Uid(path);
|
||||
var resource = await _database.Table<ResourceAttribute>().Where(r => r.Uid == uid).FirstOrDefaultAsync();
|
||||
|
||||
|
||||
if (resource == null)
|
||||
{
|
||||
_logger.LogError($"Resource not found: {path}");
|
||||
@@ -330,7 +461,7 @@ public class ResourceService
|
||||
|
||||
resource.Owner = owner;
|
||||
var rowsAffected = await _database.UpdateAsync(resource);
|
||||
|
||||
|
||||
if (rowsAffected > 0)
|
||||
{
|
||||
_logger.LogInformation($"Successfully changed ownership of '{path}' to '{owner}'");
|
||||
@@ -356,9 +487,9 @@ public class ResourceService
|
||||
_logger.LogError($"Invalid permission format: {permission}");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
var path = Path.GetRelativePath(_config.MediaRoot, fullPath);
|
||||
|
||||
|
||||
if (update)
|
||||
return await _database.InsertOrReplaceAsync(new ResourceAttribute()
|
||||
{
|
||||
|
||||
@@ -179,4 +179,12 @@ public class UserService
|
||||
var algorithm = SignatureAlgorithm.Ed25519;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,35 @@
|
||||
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
|
||||
namespace Abyss.Components.Static;
|
||||
|
||||
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)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(basePath))
|
||||
|
||||
@@ -9,7 +9,7 @@ def get_video_duration(video_path):
|
||||
"""Get video duration in milliseconds using ffprobe"""
|
||||
try:
|
||||
cmd = [
|
||||
'ffprobe',
|
||||
'ffprobe',
|
||||
'-v', 'error',
|
||||
'-show_entries', 'format=duration',
|
||||
'-of', 'default=noprint_wrappers=1:nokey=1',
|
||||
@@ -44,15 +44,15 @@ def create_thumbnails(video_path, gallery_path, num_thumbnails=10):
|
||||
except (subprocess.CalledProcessError, ValueError) as e:
|
||||
print(f"Could not get duration for '{video_path}': {e}. Skipping thumbnail creation.")
|
||||
return
|
||||
|
||||
|
||||
if duration <= 0:
|
||||
print(f"Warning: Invalid video duration for '{video_path}'. Skipping thumbnail creation.")
|
||||
return
|
||||
|
||||
interval = duration / (num_thumbnails + 1)
|
||||
|
||||
|
||||
print(f"Generating {num_thumbnails} thumbnails for {video_path.name}...")
|
||||
|
||||
|
||||
for i in range(num_thumbnails):
|
||||
timestamp = (i + 1) * interval
|
||||
output_thumbnail_path = gallery_path / f"{i}.jpg"
|
||||
@@ -68,6 +68,45 @@ def create_thumbnails(video_path, gallery_path, num_thumbnails=10):
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f" Error extracting thumbnail {i}.jpg: {e}")
|
||||
|
||||
def create_cover(video_path, output_path, time_percent):
|
||||
"""
|
||||
Creates a cover image from a video at a specified time percentage.
|
||||
"""
|
||||
try:
|
||||
subprocess.run(['ffmpeg', '-version'], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
print("Error: ffmpeg is not installed or not in your PATH. Cannot create cover.")
|
||||
return
|
||||
|
||||
try:
|
||||
duration_cmd = [
|
||||
'ffprobe', '-v', 'error', '-show_entries', 'format=duration',
|
||||
'-of', 'default=noprint_wrappers=1:nokey=1', str(video_path)
|
||||
]
|
||||
result = subprocess.run(duration_cmd, capture_output=True, text=True, check=True)
|
||||
duration = float(result.stdout)
|
||||
except (subprocess.CalledProcessError, ValueError) as e:
|
||||
print(f"Could not get duration for '{video_path}': {e}. Cannot create cover.")
|
||||
return
|
||||
|
||||
if duration <= 0:
|
||||
print(f"Warning: Invalid video duration for '{video_path}'. Cannot create cover.")
|
||||
return
|
||||
|
||||
timestamp = duration * time_percent
|
||||
|
||||
ffmpeg_cmd = [
|
||||
'ffmpeg', '-ss', str(timestamp), '-i', str(video_path),
|
||||
'-vframes', '1', str(output_path), '-y'
|
||||
]
|
||||
|
||||
print(f"Creating cover image from video at {timestamp:.2f} seconds...")
|
||||
try:
|
||||
subprocess.run(ffmpeg_cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
print(f"Cover image created at {output_path}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error creating cover image: {e}")
|
||||
|
||||
def update_summary(base_path, name_input=None, author_input=None):
|
||||
"""
|
||||
Updates the summary.json file for a given path.
|
||||
@@ -87,7 +126,7 @@ def update_summary(base_path, name_input=None, author_input=None):
|
||||
"like": 0,
|
||||
"author": author_input if author_input is not None else "anonymous"
|
||||
}
|
||||
|
||||
|
||||
# Load existing summary if available
|
||||
if summary_path.exists():
|
||||
try:
|
||||
@@ -99,13 +138,13 @@ def update_summary(base_path, name_input=None, author_input=None):
|
||||
default_summary[key] = existing_data[key]
|
||||
except json.JSONDecodeError:
|
||||
print("Warning: Invalid JSON in summary.json, using defaults")
|
||||
|
||||
|
||||
# Update duration from video file
|
||||
if video_path.exists():
|
||||
default_summary["duration"] = get_video_duration(video_path)
|
||||
else:
|
||||
print(f"Warning: video.mp4 not found at {video_path}")
|
||||
|
||||
|
||||
# Update gallery from directory
|
||||
if gallery_path.exists() and gallery_path.is_dir():
|
||||
gallery_files = []
|
||||
@@ -116,11 +155,11 @@ def update_summary(base_path, name_input=None, author_input=None):
|
||||
default_summary["gallery"] = gallery_files
|
||||
else:
|
||||
print(f"Warning: gallery directory not found at {gallery_path}")
|
||||
|
||||
|
||||
# Write updated summary
|
||||
with open(summary_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(default_summary, f, indent=4, ensure_ascii=False)
|
||||
|
||||
|
||||
print(f"Summary updated successfully at {summary_path}")
|
||||
|
||||
def find_next_directory(base_path):
|
||||
@@ -129,7 +168,7 @@ def find_next_directory(base_path):
|
||||
for item in base_path.iterdir():
|
||||
if item.is_dir() and item.name.isdigit():
|
||||
existing_dirs.add(int(item.name))
|
||||
|
||||
|
||||
next_num = 1
|
||||
while next_num in existing_dirs:
|
||||
next_num += 1
|
||||
@@ -141,8 +180,9 @@ def main():
|
||||
print("Commands:")
|
||||
print(" -u <path> Update the summary.json in the specified path.")
|
||||
print(" -a <video_file> <path> Add a new video project in a new directory under the specified path.")
|
||||
print(" -c <path> <time> Create a cover image from the video in the specified path at a given time percentage (0.0-1.0).")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
command = sys.argv[1]
|
||||
|
||||
if command == '-u':
|
||||
@@ -159,7 +199,7 @@ def main():
|
||||
if len(sys.argv) != 4:
|
||||
print("Usage: python script.py -a <video_file> <path>")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
video_source_path = Path(sys.argv[2])
|
||||
base_path = Path(sys.argv[3])
|
||||
|
||||
@@ -170,37 +210,66 @@ def main():
|
||||
if not base_path.is_dir():
|
||||
print(f"Error: Base path not found or is not a directory: {base_path}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Find a new directory name (e.g., "1", "2", "3")
|
||||
new_dir_name = find_next_directory(base_path)
|
||||
new_project_path = base_path / new_dir_name
|
||||
|
||||
|
||||
# Create the new project directory and the gallery subdirectory
|
||||
new_project_path.mkdir(exist_ok=True)
|
||||
gallery_path = new_project_path / "gallery"
|
||||
gallery_path.mkdir(exist_ok=True)
|
||||
print(f"New project directory created at {new_project_path}")
|
||||
|
||||
|
||||
# Copy video file to the new directory
|
||||
shutil.copy(video_source_path, new_project_path / "video.mp4")
|
||||
print(f"Video copied to {new_project_path / 'video.mp4'}")
|
||||
|
||||
# --- 新增功能:自动生成缩略图 ---
|
||||
# Auto-generate thumbnails
|
||||
video_dest_path = new_project_path / "video.mp4"
|
||||
create_thumbnails(video_dest_path, gallery_path)
|
||||
# ------------------------------------
|
||||
|
||||
# Get user input for name and author
|
||||
video_name = input("Enter the video name: ")
|
||||
video_author = input("Enter the author's name: ")
|
||||
# Get user input for name and author, with a prompt for default values
|
||||
print("\nEnter the video name (press Enter to use the original filename):")
|
||||
video_name = input(f"Video Name [{video_source_path.stem}]: ")
|
||||
if not video_name:
|
||||
video_name = video_source_path.stem
|
||||
|
||||
# Update the summary with user input
|
||||
print("\nEnter the author's name (press Enter to use 'Anonymous'):")
|
||||
video_author = input("Author Name [Anonymous]: ")
|
||||
if not video_author:
|
||||
video_author = "Anonymous"
|
||||
|
||||
# Update the summary with user input or default values
|
||||
update_summary(new_project_path, name_input=video_name, author_input=video_author)
|
||||
|
||||
elif command == '-c':
|
||||
if len(sys.argv) != 4:
|
||||
print("Usage: python script.py -c <path> <time>")
|
||||
sys.exit(1)
|
||||
|
||||
base_path = Path(sys.argv[2])
|
||||
video_path = base_path / "video.mp4"
|
||||
cover_path = base_path / "cover.jpg"
|
||||
|
||||
try:
|
||||
time_percent = float(sys.argv[3])
|
||||
if not 0.0 <= time_percent <= 1.0:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
print("Error: Time value must be a number between 0.0 and 1.0.")
|
||||
sys.exit(1)
|
||||
|
||||
if not video_path.exists() or not video_path.is_file():
|
||||
print(f"Error: video.mp4 not found at {video_path}")
|
||||
sys.exit(1)
|
||||
|
||||
create_cover(video_path, cover_path, time_percent)
|
||||
|
||||
else:
|
||||
print("Invalid command. Use -u or -a.")
|
||||
print("Invalid command. Use -u, -a, or -c.")
|
||||
print("Usage: python script.py <command> [arguments]")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
Reference in New Issue
Block a user