-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Telegram content proxy #163
Merged
Merged
Changes from 6 commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
9eb8f30
(#102) Web: add a dependency on Telegram link resolver
ForNeVeR afbe2f8
(#102) ContentProxy: add a FileCache
ForNeVeR 800546a
(#102) ContentProxy: finally, make it compile
ForNeVeR 3e7b422
(#102) FileCacheTests: preliminary test API
ForNeVeR 97db22a
(#102) TestFramework: extract the code from TestUtils
ForNeVeR 4d6bfc5
(#102) ContentProxy: finish working FileCache
ForNeVeR a5e26a1
(#102) FileCacheTests: implement an ordering test
ForNeVeR 6ea4892
(#102) FileCache: cache directory validation tests
ForNeVeR cb218b6
(#102) FileCache: additional tests
ForNeVeR b50d615
(#102) FileCache: finish the last tests
ForNeVeR 9100471
(#102) ContentController: test redirect mode
ForNeVeR 107c4be
(#102) ContentController: last test groundwork
ForNeVeR e8e8153
(#102) FileCache: async stream optimization
ForNeVeR 067da2d
(#102) ContentController: add last tests
ForNeVeR 9292428
(#102) ContentController: make it work in manual tests
ForNeVeR b02512c
(#102) ContentProxy: some small fixes
ForNeVeR 5d954d6
(#102) ContentProxy: add file names and MIME types
ForNeVeR 3977248
(#102) FileCache: support older versions of Windows
ForNeVeR fb5dc3a
Docs: a slight improvement
ForNeVeR a58f54e
(#102) FileCache: drop redundant rec
ForNeVeR b2cccee
(#102) FileCache: improve the workarounds for the older versions of W…
ForNeVeR 2861ee8
(#102) ContentProxy: redesign the attribute optionality
ForNeVeR 7936682
(#102) Settings: update the example
ForNeVeR File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
namespace Emulsion.ContentProxy | ||
|
||
open System | ||
open System.IO | ||
open System.Net.Http | ||
open System.Security.Cryptography | ||
open System.Text | ||
open System.Threading | ||
|
||
open Serilog | ||
open SimpleBase | ||
|
||
open Emulsion.Settings | ||
|
||
type DownloadRequest = { | ||
Uri: Uri | ||
CacheKey: string | ||
Size: uint64 | ||
} | ||
|
||
module Base58 = | ||
/// Suggested by @ttldtor. | ||
let M4N71KR = Base58(Base58Alphabet "123456789qwertyuiopasdfghjkzxcvbnmQWERTYUPASDFGHJKLZXCVBNM") | ||
|
||
module FileCache = | ||
let EncodeFileName(sha256: SHA256, cacheKey: string): string = | ||
cacheKey | ||
|> Encoding.UTF8.GetBytes | ||
|> sha256.ComputeHash | ||
|> Base58.M4N71KR.Encode | ||
|
||
let DecodeFileNameToSha256Hash(fileName: string): byte[] = | ||
(Base58.M4N71KR.Decode fileName).ToArray() | ||
|
||
type FileCache(logger: ILogger, | ||
settings: FileCacheSettings, | ||
httpClientFactory: IHttpClientFactory, | ||
sha256: SHA256) = | ||
|
||
let getFilePath(cacheKey: string) = | ||
Path.Combine(settings.Directory, FileCache.EncodeFileName(sha256, cacheKey)) | ||
|
||
let getFromCache(cacheKey: string) = async { | ||
let path = getFilePath cacheKey | ||
return | ||
if File.Exists path then | ||
Some(new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read|||FileShare.Delete)) | ||
else | ||
None | ||
} | ||
|
||
let assertCacheValid() = async { | ||
Directory.EnumerateFileSystemEntries settings.Directory | ||
|> Seq.iter(fun entry -> | ||
let entryName = Path.GetFileName entry | ||
|
||
if not <| File.Exists entry | ||
then failwith $"Cache directory invalid: contains a subdirectory: \"{entryName}\"." | ||
|
||
let hash = FileCache.DecodeFileNameToSha256Hash entryName | ||
if hash.Length <> sha256.HashSize / 8 | ||
then failwith ( | ||
$"Cache directory invalid: contains entry \"{entryName}\" which doesn't correspond to a " + | ||
"base58-encoded SHA-256 hash." | ||
) | ||
) | ||
} | ||
|
||
let ensureFreeCache size = async { | ||
if size > settings.FileSizeLimitBytes || size > settings.TotalCacheSizeLimitBytes then | ||
return false | ||
else | ||
do! assertCacheValid() | ||
|
||
let allEntries = | ||
Directory.EnumerateFileSystemEntries settings.Directory | ||
|> Seq.map FileInfo | ||
|
||
// Now, sort the entries from newest to oldest, and start deleting if required at a point when we understand | ||
// that there are too much files: | ||
let entriesByPriority = | ||
allEntries | ||
|> Seq.sortByDescending(fun info -> info.LastWriteTimeUtc) | ||
|> Seq.toArray | ||
|
||
let mutable currentSize = 0UL | ||
for info in entriesByPriority do | ||
currentSize <- currentSize + Checked.uint64 info.Length | ||
if currentSize + size > settings.TotalCacheSizeLimitBytes then | ||
logger.Information("Deleting a cache item \"{FileName}\" ({Size} bytes)", info.Name, info.Length) | ||
info.Delete() | ||
|
||
return true | ||
} | ||
|
||
let download(uri: Uri): Async<Stream> = async { | ||
let! ct = Async.CancellationToken | ||
|
||
use client = httpClientFactory.CreateClient() | ||
let! response = Async.AwaitTask <| client.GetAsync(uri, ct) | ||
return! Async.AwaitTask <| response.EnsureSuccessStatusCode().Content.ReadAsStreamAsync() | ||
} | ||
|
||
let downloadIntoCacheAndGet uri cacheKey: Async<Stream> = async { | ||
let! ct = Async.CancellationToken | ||
let! stream = download uri | ||
let path = getFilePath cacheKey | ||
logger.Information("Saving {Uri} to path {Path}…", uri, path) | ||
|
||
do! async { // to limit the cachedFile scope | ||
use cachedFile = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.None) | ||
do! Async.AwaitTask(stream.CopyToAsync(cachedFile, ct)) | ||
logger.Information("Download successful: \"{Uri}\" to \"{Path}\".") | ||
} | ||
|
||
let! file = getFromCache cacheKey | ||
return upcast Option.get file | ||
} | ||
|
||
let cancellation = new CancellationTokenSource() | ||
let processRequest request: Async<Stream> = async { | ||
logger.Information("Cache lookup for content {Uri} (cache key {CacheKey})", request.Uri, request.CacheKey) | ||
match! getFromCache request.CacheKey with | ||
| Some content -> | ||
logger.Information("Cache hit for content {Uri} (cache key {CacheKey})", request.Uri, request.CacheKey) | ||
return content | ||
| None -> | ||
logger.Information("No cache hit for content {Uri} (cache key {CacheKey}), will download", request.Uri, request.CacheKey) | ||
let! shouldCache = ensureFreeCache request.Size | ||
if shouldCache then | ||
logger.Information("Resource {Uri} (cache key {CacheKey}, {Size} bytes) will fit into cache, caching", request.Uri, request.CacheKey, request.Size) | ||
let! result = downloadIntoCacheAndGet request.Uri request.CacheKey | ||
logger.Information("Resource {Uri} (cache key {CacheKey}, {Size} bytes) downloaded", request.Uri, request.CacheKey, request.Size) | ||
return result | ||
else | ||
logger.Information("Resource {Uri} (cache key {CacheKey}) won't fit into cache, directly downloading", request.Uri, request.CacheKey) | ||
let! result = download request.Uri | ||
return result | ||
} | ||
|
||
let rec processLoop(processor: MailboxProcessor<_ * AsyncReplyChannel<_>>) = async { | ||
while true do | ||
let! request, replyChannel = processor.Receive() | ||
try | ||
let! result = processRequest request | ||
replyChannel.Reply(Some result) | ||
with | ||
| ex -> | ||
logger.Error(ex, "Exception while processing the file download queue") | ||
replyChannel.Reply None | ||
} | ||
let processor = MailboxProcessor.Start(processLoop, cancellation.Token) | ||
|
||
interface IDisposable with | ||
member _.Dispose() = | ||
cancellation.Dispose() | ||
(processor :> IDisposable).Dispose() | ||
|
||
member _.Download(uri: Uri, cacheKey: string, size: uint64): Async<Stream option> = | ||
processor.PostAndAsyncReply(fun chan -> ({ | ||
Uri = uri | ||
CacheKey = cacheKey | ||
Size = size | ||
}, chan)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<Project Sdk="Microsoft.NET.Sdk.Web"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net6.0</TargetFramework> | ||
<GenerateDocumentationFile>true</GenerateDocumentationFile> | ||
<OutputType>Library</OutputType> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<Compile Include="LockedBuffer.fs" /> | ||
<Compile Include="Logging.fs" /> | ||
<Compile Include="Waiter.fs" /> | ||
<Compile Include="TestDataStorage.fs" /> | ||
<Compile Include="Exceptions.fs" /> | ||
<Compile Include="TelegramClientMock.fs" /> | ||
<Compile Include="WebFileStorage.fs" /> | ||
<Compile Include="SimpleHttpClientFactory.fs" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Serilog.Sinks.XUnit" Version="1.0.8" /> | ||
<ProjectReference Include="..\Emulsion.Database\Emulsion.Database.fsproj" /> | ||
<ProjectReference Include="..\Emulsion.Telegram\Emulsion.Telegram.fsproj" /> | ||
</ItemGroup> | ||
</Project> |
2 changes: 1 addition & 1 deletion
2
Emulsion.Tests/TestUtils/Exceptions.fs → Emulsion.TestFramework/Exceptions.fs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
Emulsion.Tests/TestUtils/LockedBuffer.fs → Emulsion.TestFramework/LockedBuffer.fs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
Emulsion.Tests/TestUtils/Logging.fs → Emulsion.TestFramework/Logging.fs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
namespace Emulsion.TestFramework | ||
|
||
open System.Net.Http | ||
|
||
type SimpleHttpClientFactory() = | ||
interface IHttpClientFactory with | ||
member this.CreateClient _ = new HttpClient() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
namespace Emulsion.TestFramework | ||
|
||
open System.Collections.Generic | ||
|
||
open Emulsion.Telegram | ||
|
||
type TelegramClientMock() = | ||
let responses = Dictionary<string, FileInfo option>() | ||
|
||
interface ITelegramClient with | ||
member this.GetFileInfo fileId = async.Return responses[fileId] | ||
|
||
member _.SetResponse(fileId: string, fileInfo: FileInfo option): unit = | ||
responses[fileId] <- fileInfo |
2 changes: 1 addition & 1 deletion
2
Emulsion.Tests/TestUtils/TestDataStorage.fs → Emulsion.TestFramework/TestDataStorage.fs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
module Emulsion.Tests.TestUtils.TestDataStorage | ||
module Emulsion.TestFramework.TestDataStorage | ||
|
||
open System.IO | ||
|
||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably leak.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have several places with
while true
in async code already, and they seem non-problematic for now. I think I'll leave that as-is, but will keep an eye on it.