-
-
Notifications
You must be signed in to change notification settings - Fork 4
Add API for downloading media file metadata #1930
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
base: develop
Are you sure you want to change the base?
Changes from 8 commits
b16bb5d
8a24f83
e14fab2
693e720
24bf298
b46bb0d
7fa38a8
f1e5c30
a636f41
51e5622
ba611da
0ae4a8d
35e07a3
9add1da
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
using Microsoft.JSInterop; | ||
using LcmCrdt.MediaServer; | ||
using SIL.Harmony.Resource; | ||
|
||
namespace FwLiteShared.Services; | ||
|
||
public class MediaFilesServiceJsInvokable(LcmMediaService mediaService) | ||
{ | ||
[JSInvokable] | ||
public async Task<RemoteResource[]> ResourcesPendingDownload() | ||
{ | ||
return await mediaService.ResourcesPendingDownload(); | ||
} | ||
|
||
[JSInvokable] | ||
public async Task<LocalResource[]> ResourcesPendingUpload() | ||
{ | ||
return await mediaService.ResourcesPendingUpload(); | ||
} | ||
|
||
[JSInvokable] | ||
public async Task DownloadAllResources() | ||
{ | ||
await mediaService.DownloadAllResources(); | ||
} | ||
|
||
[JSInvokable] | ||
public async Task UploadAllResources() | ||
{ | ||
await mediaService.UploadAllResources(); | ||
} | ||
|
||
[JSInvokable] | ||
public async Task DownloadResources(IEnumerable<RemoteResource> resources) | ||
{ | ||
await mediaService.DownloadResources(resources); | ||
} | ||
|
||
[JSInvokable] | ||
public async Task UploadResources(IEnumerable<LocalResource> resources) | ||
{ | ||
await mediaService.UploadResources(resources); | ||
} | ||
} | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,7 @@ | |
using SIL.Harmony; | ||
using SIL.Harmony.Core; | ||
using SIL.Harmony.Db; | ||
using SIL.Harmony.Resource; | ||
using System.Runtime.CompilerServices; | ||
using FwLiteShared.AppUpdate; | ||
using FwLiteShared.Sync; | ||
|
@@ -84,7 +85,9 @@ private static void ConfigureMiniLcmTypes(ConfigurationBuilder builder) | |
typeof(RichTextObjectData), | ||
|
||
typeof(MediaFile), | ||
typeof(LcmFileMetadata) | ||
typeof(LcmFileMetadata), | ||
typeof(RemoteResource), | ||
typeof(LocalResource) | ||
], | ||
exportBuilder => exportBuilder.WithPublicNonStaticProperties(exportBuilder => | ||
{ | ||
|
@@ -112,6 +115,10 @@ private static void ConfigureMiniLcmTypes(ConfigurationBuilder builder) | |
.FlattenHierarchy() | ||
.WithPublicProperties() | ||
.WithPublicMethods(b => b.AlwaysReturnPromise().OnlyJsInvokable()); | ||
builder.ExportAsInterface<MediaFilesServiceJsInvokable>() | ||
// .WithPublicMethods(b => b.AlwaysReturnPromise().OnlyJsInvokable()); | ||
.WithPublicMethods(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ues we should always use |
||
// TODO: Does MediaFilesServiceJsInvokable need the AlwaysReturnPromise().OnlyJsInvokable() setup that MiniLcmJsInvokable needs? | ||
builder.ExportAsEnum<SortField>().UseString(); | ||
builder.ExportAsInterfaces([ | ||
typeof(QueryOptions), | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,9 @@ public interface IMediaServerClient | |
[Get("/api/media/{fileId}")] | ||
Task<HttpResponseMessage> DownloadFile(Guid fileId); | ||
|
||
[Get("/api/media/metadata/{fileId}")] | ||
Task<HttpResponseMessage> GetFileMetadata(Guid fileId); | ||
|
||
|
||
[Post("/api/media")] | ||
[Multipart] | ||
Task<MediaUploadFileResponse> UploadFile(MultipartItem file, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ | |
using LcmCrdt.RemoteSync; | ||
using Microsoft.Extensions.Logging; | ||
using MiniLcm.Media; | ||
using System.Net.Http.Json; | ||
|
||
namespace LcmCrdt.MediaServer; | ||
|
||
|
@@ -24,6 +25,16 @@ public async Task<HarmonyResource[]> AllResources() | |
return await resourceService.AllResources(); | ||
} | ||
|
||
public async Task<RemoteResource[]> ResourcesPendingDownload() | ||
{ | ||
return await resourceService.ListResourcesPendingDownload(); | ||
} | ||
|
||
public async Task<LocalResource[]> ResourcesPendingUpload() | ||
{ | ||
return await resourceService.ListResourcesPendingUpload(); | ||
} | ||
|
||
/// <summary> | ||
/// should only be used in fw-headless for files which already exist in the lexbox db | ||
/// </summary> | ||
|
@@ -42,6 +53,56 @@ public async Task DeleteResource(Guid fileId) | |
await resourceService.DeleteResource(currentProjectService.ProjectData.ClientId, fileId); | ||
} | ||
|
||
public async Task<LocalResource?> DownloadResourceIfNeeded(Guid fileId) | ||
{ | ||
var localResource = await resourceService.GetLocalResource(fileId); | ||
if (localResource is null) | ||
{ | ||
var connectionStatus = await httpClientProvider.ConnectionStatus(); | ||
if (connectionStatus == ConnectionStatus.Online) | ||
{ | ||
return await resourceService.DownloadResource(fileId, this); | ||
} | ||
} | ||
return localResource; | ||
} | ||
|
||
public async Task DownloadAllResources() | ||
{ | ||
var connectionStatus = await httpClientProvider.ConnectionStatus(); | ||
if (connectionStatus == ConnectionStatus.Online) | ||
{ | ||
var resources = await ResourcesPendingDownload(); | ||
foreach (var resource in resources) | ||
{ | ||
if (resource.RemoteId is null) continue; | ||
await resourceService.DownloadResource(resource.Id, this); | ||
} | ||
} | ||
// TODO: Gracefully handle other connection statuses, e.g. "not logged in" | ||
} | ||
|
||
public async Task UploadAllResources() | ||
{ | ||
await UploadResources(await ResourcesPendingUpload()); | ||
} | ||
|
||
public async Task DownloadResources(IEnumerable<RemoteResource> resources) | ||
{ | ||
foreach (var resource in resources) | ||
{ | ||
await DownloadResourceIfNeeded(resource.Id); | ||
} | ||
} | ||
|
||
public async Task UploadResources(IEnumerable<LocalResource> resources) | ||
{ | ||
foreach (var resource in resources) | ||
{ | ||
await ((IRemoteResourceService)this).UploadResource(resource.Id, resource.LocalPath); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// return a stream for the file, if it's not cached locally, it will be downloaded | ||
/// </summary> | ||
|
@@ -50,25 +111,56 @@ public async Task DeleteResource(Guid fileId) | |
/// <exception cref="FileNotFoundException"></exception> | ||
public async Task<ReadFileResponse> GetFileStream(Guid fileId) | ||
{ | ||
var localResource = await resourceService.GetLocalResource(fileId); | ||
var localResource = await DownloadResourceIfNeeded(fileId); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't like that we're not calling |
||
if (localResource is null) | ||
{ | ||
var connectionStatus = await httpClientProvider.ConnectionStatus(); | ||
if (connectionStatus == ConnectionStatus.Online) | ||
{ | ||
localResource = await resourceService.DownloadResource(fileId, this); | ||
// Try again, maybe earlier failure was a blip | ||
localResource = await DownloadResourceIfNeeded(fileId); | ||
} | ||
else | ||
{ | ||
return new ReadFileResponse(ReadFileResult.Offline); | ||
} | ||
} | ||
//todo, consider trying to download the file again, maybe the cache was cleared | ||
if (localResource is null || !File.Exists(localResource.LocalPath)) | ||
{ | ||
// One more attempt to download again, maybe the cache was cleared | ||
localResource = await DownloadResourceIfNeeded(fileId); | ||
// If still null then connection is offline or unreliable enough to consider as offline | ||
if (localResource is null) return new ReadFileResponse(ReadFileResult.Offline); | ||
} | ||
// If still can't find local path then this is where we give up | ||
if (!File.Exists(localResource.LocalPath)) | ||
throw new FileNotFoundException("Unable to find the file with Id" + fileId, localResource.LocalPath); | ||
return new(File.OpenRead(localResource.LocalPath), Path.GetFileName(localResource.LocalPath)); | ||
} | ||
|
||
public async Task<LcmFileMetadata> GetFileMetadata(Guid fileId) | ||
{ | ||
var mediaClient = await MediaServerClient(); | ||
var response = await mediaClient.GetFileMetadata(fileId); | ||
if (!response.IsSuccessStatusCode) | ||
{ | ||
throw new Exception($"Failed to retrieve metadata for file {fileId}: {response.StatusCode} {response.ReasonPhrase}"); | ||
} | ||
var metadata = await response.Content.ReadFromJsonAsync<LcmFileMetadata>(); | ||
if (metadata is null) | ||
{ | ||
// Try to get content into error message, but if buffering not enabled for this request, give up | ||
var content = ""; | ||
try | ||
{ | ||
content = await response.Content.ReadAsStringAsync(); | ||
} | ||
catch { } // Oh well, we tried | ||
throw new Exception($"Failed to retrieve metadata for file {fileId}: response was in incorrect format. {content}"); | ||
} | ||
return metadata; | ||
} | ||
|
||
private async Task<(Stream? stream, string? filename)> RequestMediaFile(Guid fileId) | ||
{ | ||
var mediaClient = await MediaServerClient(); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
/* eslint-disable */ | ||
// This code was generated by a Reinforced.Typings tool. | ||
// Changes to this file may cause incorrect behavior and will be lost if | ||
// the code is regenerated. | ||
|
||
import type {IRemoteResource} from '../../SIL/Harmony/Resource/IRemoteResource'; | ||
import type {ILocalResource} from '../../SIL/Harmony/Resource/ILocalResource'; | ||
|
||
export interface IMediaFilesServiceJsInvokable | ||
{ | ||
resourcesPendingDownload() : Promise<IRemoteResource[]>; | ||
resourcesPendingUpload() : Promise<ILocalResource[]>; | ||
downloadAllResources() : Promise<void>; | ||
uploadAllResources() : Promise<void>; | ||
downloadResources(resources: IRemoteResource[]) : Promise<void>; | ||
uploadResources(resources: ILocalResource[]) : Promise<void>; | ||
} | ||
/* eslint-enable */ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
/* eslint-disable */ | ||
// This code was generated by a Reinforced.Typings tool. | ||
// Changes to this file may cause incorrect behavior and will be lost if | ||
// the code is regenerated. | ||
|
||
export interface ILocalResource | ||
{ | ||
id: string; | ||
localPath: string; | ||
} | ||
/* eslint-enable */ |
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.
Expose metadata APIs through JS interop to meet PR objectives.
Per PR objectives, the backend should provide an API to fetch file metadata.
LcmMediaService
hasGetFileMetadata(Guid fileId)
andAllResources()
but the invokable does not expose them, so the frontend can’t call them.Add wrappers:
You’ll also need to export
HarmonyResource
andLcmFileMetadata
via TypeGen so the TS interface gains these signatures, and update the generatedIMediaFilesServiceJsInvokable
plus the frontend wrapper accordingly. I can draft the TypeGen config changes if helpful.📝 Committable suggestion