Merge pull request #12 from Franciskid/feature/cleanup-finished-links
Add a button to clear out finished share links, and fold 1.0.3.1 back into 1.0.3.0
Cette révision appartient à :
@@ -269,6 +269,21 @@ public sealed class ShareLinksController : ControllerBase
|
||||
return Ok(ToDto(record));
|
||||
}
|
||||
|
||||
/// <summary>Removes revoked, expired and failed share links from the store.</summary>
|
||||
[HttpPost("Admin/Cleanup")]
|
||||
[Authorize(AuthenticationSchemes = "CustomAuthentication")]
|
||||
public async Task<ActionResult> Cleanup(CancellationToken cancellationToken)
|
||||
{
|
||||
SetNoStoreHeaders();
|
||||
if (!User.IsInRole("Administrator"))
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
var removed = await _cleanupService.PurgeFinishedAsync(cancellationToken).ConfigureAwait(false);
|
||||
return Ok(new { removed });
|
||||
}
|
||||
|
||||
/// <summary>Returns the guest session state for the current authenticated user.</summary>
|
||||
[HttpGet("GuestState")]
|
||||
[Authorize(AuthenticationSchemes = "CustomAuthentication")]
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
<LangVersion>latest</LangVersion>
|
||||
<RootNamespace>Jellyfin.Plugin.ShareLinks</RootNamespace>
|
||||
<AssemblyName>Jellyfin.Plugin.ShareLinks</AssemblyName>
|
||||
<Version>1.0.3.1</Version>
|
||||
<AssemblyVersion>1.0.3.1</AssemblyVersion>
|
||||
<FileVersion>1.0.3.1</FileVersion>
|
||||
<Version>1.0.3.0</Version>
|
||||
<AssemblyVersion>1.0.3.0</AssemblyVersion>
|
||||
<FileVersion>1.0.3.0</FileVersion>
|
||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
@@ -75,6 +75,40 @@ public sealed class ShareLinkCleanupService : IShareLinkCleanupService
|
||||
await CleanupRecordInternalAsync(record, records, force, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a normal cleanup pass and then drops every record that is finished
|
||||
/// with, so the admin list does not grow forever. A link that can still be
|
||||
/// used is never removed, and neither is one whose guest is still watching.
|
||||
/// </summary>
|
||||
public async Task<int> PurgeFinishedAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Teardown first: this expires anything past its date and removes the guest
|
||||
// account and tags, so nothing is dropped from the store while it still has
|
||||
// state hanging off it.
|
||||
await CleanupAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var records = await _store.ListAsync(cancellationToken).ConfigureAwait(false);
|
||||
var removed = 0;
|
||||
foreach (var record in records)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (record.Status is not (ShareLinkStatus.Expired or ShareLinkStatus.Revoked or ShareLinkStatus.Failed))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
await _store.DeleteAsync(record.Id, cancellationToken).ConfigureAwait(false);
|
||||
removed++;
|
||||
}
|
||||
|
||||
if (removed > 0)
|
||||
{
|
||||
_logger.LogInformation("ShareLinks: purged {Count} finished share-link record(s).", removed);
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
private async Task<ShareLinkRecord> CleanupRecordInternalAsync(
|
||||
ShareLinkRecord record,
|
||||
IReadOnlyList<ShareLinkRecord> allRecords,
|
||||
|
||||
@@ -173,6 +173,9 @@
|
||||
<button is="emby-button" type="button" id="RefreshLinks" class="raised">
|
||||
<span>Refresh</span>
|
||||
</button>
|
||||
<button is="emby-button" type="button" id="CleanupLinks" class="raised">
|
||||
<span>Clean up finished links</span>
|
||||
</button>
|
||||
<span id="LinksStatus" class="sl-muted sl-list-status">Loading…</span>
|
||||
</div>
|
||||
<div style="overflow-x:auto; margin-top:0.5rem;">
|
||||
@@ -426,6 +429,30 @@
|
||||
document.querySelector('#RefreshLinks').addEventListener('click', function () {
|
||||
loadLinks();
|
||||
});
|
||||
|
||||
document.querySelector('#CleanupLinks').addEventListener('click', function () {
|
||||
var button = page.querySelector('#CleanupLinks');
|
||||
var status = page.querySelector('#LinksStatus');
|
||||
button.disabled = true;
|
||||
status.textContent = 'Cleaning up…';
|
||||
ApiClient.ajax({
|
||||
type: 'POST',
|
||||
url: ApiClient.getUrl('ShareLinks/Admin/Cleanup'),
|
||||
dataType: 'json'
|
||||
}).then(function (result) {
|
||||
var removed = result && (result.removed !== undefined ? result.removed : result.Removed);
|
||||
removed = parseInt(removed, 10) || 0;
|
||||
return loadLinks().then(function () {
|
||||
page.querySelector('#LinksStatus').textContent = removed === 1
|
||||
? '1 finished link removed.'
|
||||
: removed + ' finished links removed.';
|
||||
});
|
||||
}).catch(function () {
|
||||
status.textContent = 'Cleanup failed.';
|
||||
}).finally(function () {
|
||||
button.disabled = false;
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"guid": "68540b76-ee74-436d-85ff-2abc884bbea6",
|
||||
"name": "ShareLinks",
|
||||
"version": "1.0.3.1",
|
||||
"version": "1.0.3.0",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"framework": "net9.0",
|
||||
"owner": "Franciskid",
|
||||
"overview": "Secure expiring guest-share links for Jellyfin items.",
|
||||
"description": "Adds secure, expiring share links for Jellyfin items with JSON-backed storage, token hashing, and cleanup scaffolding.",
|
||||
"category": "General",
|
||||
"timestamp": "2026-07-26T20:00:00.0000000Z"
|
||||
"timestamp": "2026-07-26T20:15:00.0000000Z"
|
||||
}
|
||||
|
||||
Référencer dans un nouveau ticket
Bloquer un utilisateur