Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
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
11 changes: 6 additions & 5 deletions config/slskd.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,18 +156,19 @@
# role: readonly # readonly, readwrite, administrator
# cidr: 0.0.0.0/0,::/0
# retention:
# searches: 10080 # 7 days, in minutes
# transfers:
# upload:
# succeeded: 1440 # 1 day
# succeeded: 1440 # 1 day, in minutes
# errored: 30
# cancelled: 5
# download:
# succeeded: 1440 # 1 day
# errored: 20160 # 2 weeks
# succeeded: 1440 # 1 day, in minutes
# errored: 20160 # 2 weeks, in minutes
# cancelled: 5
# files:
# complete: 20160 # 2 weeks
# incomplete: 43200 # 30 days
# complete: 20160 # 2 weeks, in minutes
# incomplete: 43200 # 30 days, in minutes
# logs: 180 # days
# logger:
# disk: false
Expand Down
5 changes: 4 additions & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -895,17 +895,20 @@ filters:

By default, most things created by the application are retained indefinitely; they have to be removed manually by the user. Users can optionally configure certain things to be removed or deleted automatically after a period of time.

Completed searches can be configured to be removed from the UI and 'hard' deleted from the database. It's a good idea to set this to something relatively short, as old searches have diminishing value as they age and the records contain a lot of data.

Transfers can be configured to be removed from the UI after they are complete by specifying retention periods for uploads and downloads separately, and by transfer state; succeeded, errored, and cancelled.

Files (on disk) can be configured to be deleted after the age of their last access time exceeds the configured time. Completed and incomplete files can be configured separately.

Application logs are removed after 180 days by default, but this can be configured as well.

All retention periods are specified in minutes.
All retention periods are specified in minutes, with the exception of `logs`, which is in days.

#### **YAML**
```yaml
retention:
search: 1440 # 1 day
transfers:
upload:
succeeded: 1440 # 1 day
Expand Down
22 changes: 22 additions & 0 deletions src/slskd/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ public Application(
IUserService userService,
IMessagingService messagingService,
IShareService shareService,
ISearchService searchService,
IPushbulletService pushbulletService,
IRelayService relayService,
EventService eventService,
Expand Down Expand Up @@ -146,6 +147,8 @@ public Application(
Shares = shareService;
Shares.StateMonitor.OnChange(state => ShareState_OnChange(state));

Search = searchService;

Events = eventService;

Transfers = transferService;
Expand Down Expand Up @@ -227,6 +230,7 @@ public Application(
private IHubContext<LogsHub> LogHub { get; set; }
private IUserService Users { get; set; }
private IShareService Shares { get; set; }
private ISearchService Search { get; set; }
private EventService Events { get; }
private IRelayService Relay { get; set; }
private IMemoryCache Cache { get; set; } = new MemoryCache(new MemoryCacheOptions());
Expand Down Expand Up @@ -1003,6 +1007,7 @@ private void Clock_EveryMinute(object sender, ClockEventArgs e)

private void Clock_EveryFiveMinutes(object sender, ClockEventArgs e)
{
_ = Task.Run(() => PruneSearches());
_ = Task.Run(() => PruneTransfers());
}

Expand Down Expand Up @@ -1142,6 +1147,23 @@ void PruneDownload(int? age, TransferStates state)
}
}

private async Task PruneSearches()
{
var age = OptionsMonitor.CurrentValue.Retention.Search;

if (age.HasValue)
{
try
{
var pruned = await Search.PruneAsync(age.Value);
}
catch
{
Log.Error("Encountered one or more errors while pruning searches");
}
}
}

private void Client_ExcludedSearchPhrasesReceived(object sender, IReadOnlyCollection<string> e)
{
Log.Debug("Excluded search phrases: {Phrases}", string.Join(", ", e));
Expand Down
6 changes: 6 additions & 0 deletions src/slskd/Core/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1311,6 +1311,12 @@ public class LoggerOptions
/// </summary>
public class RetentionOptions
{
/// <summary>
/// Gets the time to retain searches, in minutes.
/// </summary>
[Range(5, maximum: int.MaxValue)]
public int? Search { get; init; } = null;

/// <summary>
/// Gets transfer retention options.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions src/slskd/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,7 @@ private static void ConfigureGlobalLogger()
.MinimumLevel.Override("System.Net.Http.HttpClient", OptionsAtStartup.Debug ? LogEventLevel.Warning : LogEventLevel.Fatal)
.MinimumLevel.Override("slskd.Authentication.PassthroughAuthenticationHandler", LogEventLevel.Warning)
.MinimumLevel.Override("slskd.Authentication.ApiKeyAuthenticationHandler", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.EntityFrameworkCore.Database.Command", LogEventLevel.Warning) // bump this down to Information to show SQL
.Enrich.WithProperty("InstanceName", OptionsAtStartup.InstanceName)
.Enrich.WithProperty("InvocationId", InvocationId)
.Enrich.WithProperty("ProcessId", ProcessId)
Expand Down
2 changes: 1 addition & 1 deletion src/slskd/Search/API/Controllers/SearchesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ public async Task<IActionResult> Delete([FromRoute] Guid id)
return Forbid();
}

var search = await Searches.FindAsync(search => search.Id == id, includeResponses: true);
var search = await Searches.FindAsync(search => search.Id == id, includeResponses: false);

if (search == default)
{
Expand Down
29 changes: 25 additions & 4 deletions src/slskd/Search/API/Hubs/SearchHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,19 @@ public static class SearchHubExtensions
/// <summary>
/// Broadcast an update for a search.
/// </summary>
/// <remarks>
/// Responses are removed prior to sending.
/// </remarks>
/// <param name="hub">The hub.</param>
/// <param name="search">The search to broadcast.</param>
/// <returns>The operation context.</returns>
public static Task BroadcastUpdateAsync(this IHubContext<SearchHub> hub, Search search)
{
return hub.Clients.All.SendAsync(SearchHubMethods.Update, search);
return hub.Clients.All.SendAsync(SearchHubMethods.Update, search with { Responses = null });
}

/// <summary>
/// Broadcast the present application options.
/// Broadcast a newly received search response.
/// </summary>
/// <param name="hub">The hub.</param>
/// <param name="searchId">The ID of the search associated with the response.</param>
Expand All @@ -59,14 +62,32 @@ public static Task BroadcastResponseAsync(this IHubContext<SearchHub> hub, Guid
return hub.Clients.All.SendAsync(SearchHubMethods.Response, new { searchId, response });
}

/// <summary>
/// Broadcast the creation of a new search.
/// </summary>
/// <remarks>
/// Responses are removed prior to sending.
/// </remarks>
/// <param name="hub">The hub.</param>
/// <param name="search">The search to broadcast.</param>
/// <returns>The operation context.</returns>
public static Task BroadcastCreateAsync(this IHubContext<SearchHub> hub, Search search)
{
return hub.Clients.All.SendAsync(SearchHubMethods.Create, search);
return hub.Clients.All.SendAsync(SearchHubMethods.Create, search with { Responses = null });
}

/// <summary>
/// Broadcast the deletion of a search.
/// </summary>
/// <remarks>
/// Responses are removed prior to sending.
/// </remarks>
/// <param name="hub">The hub.</param>
/// <param name="search">The search to broadcast.</param>
/// <returns>The operation context.</returns>
public static Task BroadcastDeleteAsync(this IHubContext<SearchHub> hub, Search search)
{
return hub.Clients.All.SendAsync(SearchHubMethods.Delete, search);
return hub.Clients.All.SendAsync(SearchHubMethods.Delete, search with { Responses = null });
}
}

Expand Down
71 changes: 0 additions & 71 deletions src/slskd/Search/ISearchService.cs

This file was deleted.

98 changes: 95 additions & 3 deletions src/slskd/Search/SearchService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,59 @@ namespace slskd.Search
using SearchScope = Soulseek.SearchScope;
using SearchStates = Soulseek.SearchStates;

/// <summary>
/// Handles the lifecycle and persistence of searches.
/// </summary>
public interface ISearchService
{
/// <summary>
/// Deletes the specified search.
/// </summary>
/// <param name="search">The search to delete.</param>
/// <returns>The operation context.</returns>
Task DeleteAsync(Search search);

/// <summary>
/// Finds a single search matching the specified <paramref name="expression"/>.
/// </summary>
/// <param name="expression">The expression to use to match searches.</param>
/// <param name="includeResponses">A value indicating whether to include search responses in the result.</param>
/// <returns>The found search, or default if not found.</returns>
/// <exception cref="ArgumentException">Thrown when an expression is not supplied.</exception>
Task<Search> FindAsync(Expression<Func<Search, bool>> expression, bool includeResponses = false);

/// <summary>
/// Returns a list of all completed and in-progress searches, with responses omitted, matching the optional <paramref name="expression"/>.
/// </summary>
/// <param name="expression">An optional expression used to match searches.</param>
/// <returns>The list of searches matching the specified expression, or all searches if no expression is specified.</returns>
Task<List<Search>> ListAsync(Expression<Func<Search, bool>> expression = null);

/// <summary>
/// Performs a search for the specified <paramref name="query"/> and <paramref name="scope"/>.
/// </summary>
/// <param name="id">A unique identifier for the search.</param>
/// <param name="query">The search query.</param>
/// <param name="scope">The search scope.</param>
/// <param name="options">Search options.</param>
/// <returns>The completed search.</returns>
Task<Search> StartAsync(Guid id, SearchQuery query, SearchScope scope, SearchOptions options = null);

/// <summary>
/// Cancels the search matching the specified <paramref name="id"/>, if it is in progress.
/// </summary>
/// <param name="id">The unique identifier for the search.</param>
/// <returns>A value indicating whether the search was successfully cancelled.</returns>
bool TryCancel(Guid id);

/// <summary>
/// Removes <see cref="SearchStates.Completed"/> searches older than the specified <paramref name="age"/>.
/// </summary>
/// <param name="age">The age after which records are eligible for pruning, in minutes.</param>
/// <returns>The number of pruned records.</returns>
Task<int> PruneAsync(int age);
}

/// <summary>
/// Handles the lifecycle and persistence of searches.
/// </summary>
Expand Down Expand Up @@ -194,6 +247,8 @@ void UpdateAndSaveChanges(Search search)
search.FileCount = args.Search.FileCount;
search.LockedFileCount = args.Search.LockedFileCount;

// note that we're not actually doing anything with the response here, that's happening in the
// response handler. we're only updating counts here.
SearchHub.BroadcastUpdateAsync(search);
UpdateAndSaveChanges(search);
}));
Expand Down Expand Up @@ -225,9 +280,9 @@ void UpdateAndSaveChanges(Search search)

UpdateAndSaveChanges(search);

// zero responses before broadcasting
search.Responses = Enumerable.Empty<Response>();
await SearchHub.BroadcastUpdateAsync(search);
// zero responses before broadcasting, as we don't want to blast this
// data out over the SignalR socket
await SearchHub.BroadcastUpdateAsync(search with { Responses = [] });
}
catch (Exception ex)
{
Expand Down Expand Up @@ -256,5 +311,42 @@ public bool TryCancel(Guid id)

return false;
}

/// <summary>
/// Removes <see cref="SearchStates.Completed"/> searches older than the specified <paramref name="age"/>.
/// </summary>
/// <param name="age">The age after which searches are eligible for pruning, in minutes.</param>
/// <returns>The number of pruned records.</returns>
public async Task<int> PruneAsync(int age)
{
try
{
using var context = ContextFactory.CreateDbContext();

var cutoffDateTime = DateTime.UtcNow.AddMinutes(-age);

// unlike other pruning operations, we don't care about state, since there's a 60 minute minimum
// and searches are guaranteed to be at least 60 minutes old by the time they can be pruned, they will
// be completed unless someone applied some rather dumb settings
var expired = context.Searches
.Where(s => s.EndedAt.HasValue && s.EndedAt.Value < cutoffDateTime)
.WithoutResponses()
.ToList();

// defer the deletion to DeleteAsync() so that SignalR broadcasting works properly and the UI
// is updated in real time
foreach (var search in expired)
{
await DeleteAsync(search);
}

return expired.Count;
}
catch (Exception ex)
{
Log.Error(ex, "Failed to prune searches: {Message}", ex.Message);
throw;
}
}
}
}
2 changes: 1 addition & 1 deletion src/slskd/Search/Types/Search.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ namespace slskd.Search
using System.Text.Json.Serialization;
using Soulseek;

public class Search
public record Search
{
public DateTime? EndedAt { get; set; }
public int FileCount { get; set; }
Expand Down
Loading
Loading