I'm trying to access images in Firebase Storage and cache them locally on the device.
My current attempt uses flutter_cache_manager. The documentation states:
Most common file service will be an [HttpFileService], however one can also make something more specialized. For example you could fetch files from other apps or from local storage.
class HttpFileService implements FileService {
http.Client _httpClient;
HttpFileService({http.Client httpClient}) {
_httpClient = httpClient ?? http.Client();
}
#override
Future<FileServiceResponse> get(String url,
{Map<String, String> headers = const {}}) async {
final req = http.Request('GET', Uri.parse(url));
req.headers.addAll(headers);
final httpResponse = await _httpClient.send(req);
return HttpGetResponse(httpResponse);
}
}
I've tried to extend this class to process the URL for Firebase
class FirebaseHttpFileService extends HttpFileService {
#override
Future<FileServiceResponse> get(String url, {Map<String, String> headers = const {}}) async {
var ref = FirebaseStorage.instance.ref().child(url);
var _url = await ref.getDownloadURL() as String;
return super.get(_url);
}
}
And extend the BaseCacheManager using a template from the GitHub repo, replacing the file service with my new one.
class FirebaseCacheManager extends BaseCacheManager {
static const key = "firebaseCache";
static FirebaseCacheManager _instance;
factory FirebaseCacheManager() {
if (_instance == null) {
_instance = new FirebaseCacheManager._();
}
return _instance;
}
FirebaseCacheManager._() : super(key,
maxAgeCacheObject: Duration(days: 7),
maxNrOfCacheObjects: 20,
fileService: FirebaseHttpFileService());
Future<String> getFilePath() async {
var directory = await getTemporaryDirectory();
return p.join(directory.path, key);
}
}
But I get the following error:
setState() called after dispose(): _ImageState#50d41(lifecycle state: defunct, not mounted, stream: ImageStream#ac6d5(MultiFrameImageStreamCompleter#0c956, [2448×3264] # 1.0x, 3 listeners), pixels: null, loadingProgress: null, frameNumber: null, wasSynchronouslyLoaded: false)
I can process the URL before attempting to retrieve the file but that needlessly wastes time. I've also tried to use other packages like Flutter Cache Image but it seems to crash the app after a short amount of time.
Thanks for any pointers in the right direction!
This problem is actually tied to the errorWidget as seen in the issue here.
The code is working if the error widget is commented out in CachedNetworkImage.
Related
According to the Discord documentation, webhooks must validate the headers on every request in order to be accepted. The documentation provides the following code sample:
const nacl = require('tweetnacl');
// Your public key can be found on your application in the Developer Portal
const PUBLIC_KEY = 'APPLICATION_PUBLIC_KEY';
const signature = req.get('X-Signature-Ed25519');
const timestamp = req.get('X-Signature-Timestamp');
const body = req.rawBody; // rawBody is expected to be a string, not raw bytes
const isVerified = nacl.sign.detached.verify(
Buffer.from(timestamp + body),
Buffer.from(signature, 'hex'),
Buffer.from(PUBLIC_KEY, 'hex')
);
if (!isVerified) {
return res.status(401).end('invalid request signature');
}
How do you do this in .NET 5.0? I haven't been able to find any examples of Ed25519 validation.
This implementation requires the NSec.Cryptography NuGet package.
First, you must create an ActionFilter to place on your WebAPI controller or endpoints. The simplest way to do this is by extending ActionFilterAttribute:
public class DiscordAuthorizationActionFilterAttribute : ActionFilterAttribute
{
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// This is needed to move the request stream to the beginning as it has already been evaluated for model binding
context.HttpContext.Request.Body.Seek(0, SeekOrigin.Begin);
var signature = context.HttpContext.Request.Headers["X-Signature-Ed25519"].FirstOrDefault();
var timestamp = context.HttpContext.Request.Headers["X-Signature-Timestamp"].FirstOrDefault();
var body = await new StreamReader(context.HttpContext.Request.Body).ReadToEndAsync();
var key = "{YOUR API KEY HERE}";
var algorithm = SignatureAlgorithm.Ed25519;
var publicKey = PublicKey.Import(algorithm, GetBytesFromHexString(key), KeyBlobFormat.RawPublicKey);
var data = Encoding.UTF8.GetBytes(timestamp + body);
var verified = algorithm.Verify(publicKey, data, GetBytesFromHexString(signature));
if (!verified)
context.Result = new UnauthorizedObjectResult("Invalid request");
else
await next();
}
private byte[] GetBytesFromHexString(string hex)
{
var length = hex.Length;
var bytes = new byte[length / 2];
for (int i = 0; i < length; i += 2)
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
return bytes;
}
}
Note this comment:
// This is needed to move the request stream to the beginning as it has already been evaluated for model binding
context.HttpContext.Request.Body.Seek(0, SeekOrigin.Begin);
To allow the request body stream to be reused, you must explicitly enable this in the request pipeline prior to the first time it is accessed, likely during model binding. To do this, you can add a simple step in Startup.cs:
public void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
{
app.UseStaticFiles();
app.UseRouting();
// This is needed to retrieve request body as JSON string in ActionFilter
app.Use(async (context, next) =>
{
var controller = context.Request.RouteValues["controller"] as string;
if (controller == "Discord")
context.Request.EnableBuffering();
await next();
});
app.UseEndpoints(endpoints => endpoints.MapControllers());
}
Notice the check for the controller name; since I placed the attribute on DiscordController, the controller value stored in the RouteValues collection is "Discord".
Finally, simply add the attribute to an endpoint that accepts POST requests:
public class DiscordController : ControllerBase
{
[DiscordAuthorizationActionFilter]
[HttpPost]
public async Task<IActionResult> PostAsync(DiscordInteraction interaction)
{
if (interaction == null)
return BadRequest();
if (interaction.Type == DiscordInteractionType.Ping)
return Ok(new { Type = 1 });
// Request processing here
return Ok();
}
}
Note that the DiscordInteraction model is custom code not available in any libraries that I'm aware of. It is simple to create this by following the documentation. To test this, I found it helpful to use ngrok to route requests from Discord to my development environment.
i am new to flutter and firebase development, so i really don't know how much will it cost me to keep fetching user data from firebase in every screen that i need them in, so i decided to fetch them once and store them in class MyUser static variables as follows:
in MyApp class:
bool isAuthenticated = false;
Future checkAuthenticity() async {
AuthService.getCurrentUser().then((user) async {
if (user != null) {
String myUid = await AuthService.getCurrentUID();
await MyUserController().getCurrentUserFromFirebase(myUid);
if (mounted)
setState(() {
isAuthenticated = true;
});
} else {
if (mounted)
setState(() {
isAuthenticated = false;
});
}
});
}
#override
Widget build(BuildContext context) {
home: isAuthenticated ? Home(passedSelectedIndex: 0) : Register(),
}
from the above code, this line await MyUserController().getCurrentUserFromFirebase(myUid); is as follows:
getCurrentUserFromFirebase(String uid) async {
await FirestoreService().getCurrentUserData(uid);
}
from the above code, this line await FirestoreService().getCurrentUserData(uid); is as follows:
Future getCurrentUserData(String uid) async {
try {
var userData = await FirebaseFirestore.instance.collection('users').doc(uid).get();
MyUser.fromData(userData.data());
} catch (e) {
if (e is PlatformException) {
return e.message;
}
return e.toString();
}
}
from the above code, this line MyUser.fromData(userData.data()); is a constructor in
MyUser class as follows:
class MyUser {
static String uid;
static String name;
static String username;
static String email;
static String userAvatarUrl;
static String location;
static String phoneNumber;
MyUser.fromData(Map<String, dynamic> data) {
uid = data['id'];
name = data['name'];
username = data['username'];
email = data['email'];
userAvatarUrl = data['userAvatarUrl'];
location = data['location'];
phoneNumber = data['phoneNumber'];
}
}
and to make use of all of the following, in each page that i need to load the current user data in, i use for example:
var userId = MyUser.uid
or to show the current user name i use Text('${MyUser.name}');
when i close the app completely and relaunch it again, it should check for authenticity, and complete executing the rest of the code in main() function.
so my questions are:
1) does this have any performance issues when we release the app?
2) does this will really will prevent unnecessary reads that i can consume in every page i need the data in ?
3) is there any better approach to prevent unnecessary reads from firebase, for example to save the current user data as strings and a profile image locally?
pardon me for prolonging the question, but i wanted to share the code itself.
any help would be much appreciated.
As a short answer,
You can make a class of SharedPreferences to store data as strings in key: value manner.
So anywhere you want you can get an instance of that class and reach it from anywhere in the app.
If you also declare some functions which will decode string to json you will get a ready user class instance in return of your function which will make it easier.
So when you want to save user info to Local Storage(SharedPreferences) you may use a function which will encode your User object to string and save it to SharedPreferences as below..
user.dart' as theUser; for conflict issues
class SharedPrefs {
static SharedPreferences _sharedPrefs;
init() async {
if (_sharedPrefs == null) {
_sharedPrefs = await SharedPreferences.getInstance();
}
}
dynamic get user=> _sharedPrefs.getString('user')!=null?theUser.User.fromString(_sharedPrefs.getString('user')):null;
set user(theUser.User user)=> _sharedPrefs.setString('user', jsonEncode(user));
String get accessToken=> _sharedPrefs.getString('access_token');
set accessToken(String accessToken)=> _sharedPrefs.setString('access_token', accessToken);
void removeString(String entry){
_sharedPrefs.remove(entry);
}
}
final sharedPrefs = SharedPrefs();
And in the app anywhere you can use it directly by typing sharedPrefs.user
I am trying to allow users to upload an image for their profile pic. I want to store it inside of my azure-blob-storage. So after doing some research and going through different theories about doing this solely within the front end, I have decided to just pass the file to the backend and make the backend post to my azure blob. However, upon doing so, I get a 500 Internal Server error while attempting to upload a selected file.
I am using Angular 8 for my frontend code and using C#/ASP.NetCore for my backend. I have been able to successfully post an image to my azure-blob-storage with just my backend by using PostMan to see if my controller works. The main issue is getting my frontend code to pass this file to my controller which will handle posting to the azure-blob-storage.
I am using a service to provide a linkage between my upload-picture-component and the backend controller.
FrontEnd(Angular8)
'upload-profile-service.ts' snippet:
import { HttpClient, HttpEvent, HttpParams, HttpRequest } from '#angular/common/http';
import { Observable } from 'rxjs';
import { Injectable } from '#angular/core';
import { environment } from '#env/environment';
#Injectable({
providedIn: 'root'
})
export class UploadProfileImageService {
// dependency injection
constructor(private _httpClient: HttpClient) { }
private baseurl = environment.api + '/myupload';
// change from any to type image.
public getImages(): Observable<any> {
return this._httpClient.get(this.baseurl);
}
// Form Data as image to pass to API to be pushed to Azure Storage
public postImages(formData: FormData): Observable<any> {
const saveImageUrl = this.baseurl + '/SaveFile';
return this._httpClient.post<any>(saveImageUrl, formData);
}
'upload-profile-component.ts' snippet:
constructor(
private consultantStore: ConsultantStore,
private notification: NotificationsService,
private dialogRef: MatDialogRef<Upload-Picture-Component>,
private _uploadProfileImageService: UploadProfileImageService,
private formBuilder: FormBuilder
) { }
ngOnInit(){}
selectedFile: File = null;
imageChangedEvent: any = '';
fileChangeEvent(event: any): void {
this.imageChangedEvent = event;
this.selectedFile = <File>this.imageChangedEvent.target.files[0];
}
UploadImageToBlob(){
const formData = new FormData();
formData.append(this.selectedFile.name, this.selctedFile, this.selectedFile.name);
this._uploadProfileImageService.postImages(formData)
.subscribe(res => {
console.log(res);
})
}
BackEnd(C#)
'UploadPicController.cs' snippet
[Route("myupload")]
[ApiController]
public class FileUploadController : Controller
{
private string _conn = <my_key_to_azure_blob_storage>;
private CloudBlobClient _blobClient;
private CloudBlobContainer _container;
private CloudStorageAccount _storageAccount;
private CloudBlockBlob _blockBlob;
[HttpPost("[Action]")]
public async Task<IActionResult> SaveFile(IFormFile files)
{
_storageAccount = CloudStorageAccount.Parse(_conn);
_blobClient = _storageAccount.CreateCloudBlobClient();
_container = _blobClient.GetContainerReference("profileimages");
//Get a reference to a blob
_blockBlob = _container.GetBlockBlobReference(files.FileName);
//Create or overwrite the blob with contents of a local file
using (var fileStream = files.OpenReadStream())
{
await _blockBlob.UploadFromStreamAsync(fileStream);
}
return Json(new
{
name = _blockBlob.Name,
uri = _blockBlob.Uri,
size = _blockBlob.Properties.Length
});
}
}
I want my azure blob to be able to receive the image via httpPost when the UploadImageToBlob function is called, but instead, I receive this error...
zone.js:3372 POST http://localhost:5000/myupload/SaveFile 500 (Internal Server Error)...
core.js:5847 ERROR HttpErrorResponse {headers: HttpHeaders, status: 500, statusText: "Internal Server Error", url: "http://localhost:5000/myupload/SaveFile", ok: false, …}error: "
↵http://localhost:5000/myupload/SaveFile: 500 Internal Server Error"name: "HttpErrorResponse"ok: falsestatus: 500statusText: "Internal Server Error"url: "http://localhost:5000/myupload/SaveFile"proto: HttpResponseBase...
Here is an update on what I get in the error log in Developer Tools
'NullReferenceException: Object reference not set to an instance of an object.'
Developer Tools -> 'Preview'
Here is my HttpPost method which worked for me:
[HttpPost]
public async Task<IActionResult> UploadFileAsync([FromForm]IFormFile file)
{
CloudStorageAccount storageAccount = null;
if (CloudStorageAccount.TryParse(_configuration.GetConnectionString("StorageAccount"), out storageAccount))
{
var client = storageAccount.CreateCloudBlobClient();
var container = client.GetContainerReference("fileupload");
await container.CreateIfNotExistsAsync();
CloudBlockBlob blob = container.GetBlockBlobReference(file.FileName);
await blob.UploadFromStreamAsync(file.OpenReadStream());
return Ok(blob.Uri);
}
return StatusCode(StatusCodes.Status500InternalServerError);
}
I was also getting the same issue when i tried to use below method:
GetBlockBlobReference() or `GetBlobReferenceFromServerAsync`
Also i would suggest you to add below line after getting the container reference:
await container.CreateIfNotExistsAsync();
When I debugged the code in network tab in developer tools while i was using GetBlobReferenceFromServerAsync()
here is the reason which was causing 500:
Please try to debug it from your end and see if it helps.
Let me know if you any assistance, will share my code base.
I'm currently developing a app in which we have a feature called "tips" which are essentially microblogs. In these blogs which are html based you can link to other "Content" that exists on our app through ordinary hyperlinks.
The app is set up to open the url scheme of these links and all platforms have their own code to funnel the new uri into the xamarin forms app via the MessagingCenter which has a subcriber inside of the App class like this:
MessagingCenter.Subscribe(Current, DoshiMessages.NewUri, async (Application app, string uri) =>
{
var result = await HandleUriNavAsync(uri);
if (!result)
await Container.Resolve<IUserDialogs>().AlertAsync("Link was invalid");
});
Which calls:
private async Task<bool> HandleUriNavAsync(string uri)
{
if (uri == null)
return false;
await NavigationService.NavigateAsync(new Uri(uri, UriKind.Absolute));
return true;
}
Now this works fine with absolute uri's but if i change it to
//Example: uri = www.example.com/main/nav/test?userid=0
private async Task<bool> HandleUriNavAsync(string uri)
{
if (uri == null)
return false;
//Example: relative = test?userid=0
var relative = uri.Split('/').Last();
await NavigationService.NavigateAsync(new Uri(relative, UriKind.Relative));
return true;
}
Navigation never triggers but no exceptions are thrown and the task finishes successfully(I have checked). Note the above code is executed while the app is already running and already has active viewmodels.
Is there something obvious I'm doing wrong and is there a better way of achieving this?
Cheers!
I test your code and get the result like the following screenshot, this issue is related to the uri type.
You have set the UriKind.Absolute, so you uri type should be www.example.com/main/nav/test?userid=0.
If you set the UriKind.Relative, after spliting uri, this type of uri could be setted.
In the end, No exceptions are thrown in VS, that's so wired.
So i ended up with a workaround which works pretty good.
It's a bit sketchy but because all my viewmodels inherit a common base class
i can get the current viewmodel with some help of Prism PageUtils
MessagingCenter.Subscribe(Current, DoshiMessages.NewUri, async (Application app, string uri) =>
{
try
{
var relative = uri.Split('/').LastOrDefault();
if(relative != null)
{
var currentVM = PageUtilities.GetCurrentPage(MainPage).BindingContext as ViewModelBase;
await currentVM.HandleNewUriAsync(relative);
}
}
catch(Exception ex)
{
Crashes.TrackError(ex);
}
});
And then just perform the navigation in the current viewmodel
public async Task HandleNewUriAsync(string uri)
{
await _navigationService.NavigateAsync(new Uri(uri, UriKind.Relative));
}
I am currently uploading a file via the kendo fileuploader to an api controller using ASP.NET core RC-1. I am receiving a periodic error of "object reference not set to instance of object" when attempting to read the stream following opening the stream with IFormFile.OpenReadStream().
My controller is:
[HttpPost]
[Route("api/{domain}/[controller]")]
public async Task<IActionResult> Post([FromRoute]string domain, [FromForm]IFormFile file, [FromForm]WebDocument document)
{
if (ModelState.IsValid)
{
if (file.Length > 0)
{
var userName =
Request.HttpContext.User.Claims
.FirstOrDefault(c => c.Type == ClaimTypesEx.FullName)?
.Value;
var uploadedFileName =
ContentDispositionHeaderValue.Parse(file.ContentDisposition).FileName.Trim('"');
document.Domain = domain;
document.MimeType = file.ContentType;
document.SizeInBytes = file.Length;
document.ChangedBy = userName;
document.FileName = (string.IsNullOrEmpty(document.FileName)) ? uploadedFileName : document.FileName;
try
{
document = await CommandStack.For<WebDocument>()
.AddOrUpdateAsync(document, file.OpenReadStream()).ConfigureAwait(false);
}
catch (Exception e)
{
return new HttpStatusCodeResult(500);
}
return Ok(document);
}
}
return new BadRequestResult();
}
And the error is being thrown when I actually try to read the stream when it is going into blob storage:
public async Task<Uri> CreateOrUpdateBlobAsync(string containerName, string fileName, string mimeType,
Stream fileStream)
{
var container = Client.GetContainerReference(containerName);
var blob = container.GetBlockBlobReference(fileName);
//Error HERE
await blob.UploadFromStreamAsync(fileStream);
blob.Properties.ContentType = mimeType;
await blob.SetPropertiesAsync();
return blob.Uri;
}
What I am having trouble with is this is sporadic and there seems to be no defined pattern of which files are accepted and which ones generate the error. At first I thought it might be a size issue but that is not the case as I have several larger files uploaded successfully and then one small file will throw the error. Images seem to work fine and it is hit or miss on other file types with no rhyme or reason that I can figure out.