firebase / firebase-ios-sdk

Firebase SDK for Apple App Development
https://firebase.google.com
Apache License 2.0
5.62k stars 1.48k forks source link

Cannot unset/remove customMetadata keys on StorageReference objects - regression from v8 #9849

Closed mikehardy closed 2 years ago

mikehardy commented 2 years ago

Step 0: Are you in the right place?

[REQUIRED] Step 1: Describe your environment

[REQUIRED] Step 2: Describe the problem

I'm forward-porting react-native-firebase to the v9+ SDK release here, and I've run into a problem with Storage.

Specifically, we attempt to mirror the firebase-js-sdk API surface area to react-native library consumers (for interoperability with web code) but we call down to underlying firebase-ios-sdk and firebase-android-sdk

firebase-js-sdk documents that to remove a customMetadata key/value pair on a StorageReference, you send in a null value to the updateMetadata call.

This worked fine with firebase-ios-sdk v8 and lower, but now it appears there are some Swift-y types / casts happening, and nulls will crash an app

I will attempt to highlight the important frames 7-16 from the boilerplate with a lot of newlines:


    updateMetadata
16:13:27.013 detox[39601] ERROR: [WS_ERROR] The app has crashed, see the details below:

Signal 6 was raised
(
        0   Detox                               0x0000000102125c15 +[NSThread(DetoxUtils) dtx_demangledCallStackSymbols] + 37
        1   Detox                               0x0000000102128670 __DTXHandleCrash + 464
        2   Detox                               0x0000000102128db1 __DTXHandleSignal + 59
        3   libsystem_platform.dylib            0x00007fff701bddfd _sigtramp + 29
        4   ???                                 0x00007fc7a2723a60 0x0 + 140495400614496
        5   libsystem_c.dylib                   0x00007fff2010b6b7 abort + 130
        6   libswiftCore.dylib                  0x00007fff30c9dc0c swift::fatalError(unsigned int, char const*, ...) + 252

        7   libswiftCore.dylib                  0x00007fff30c95f17 swift::swift_dynamicCastFailure(void const*, char const*, void const*, char const*, char const*) + 71
        8   libswiftCore.dylib                  0x00007fff30c95f8a swift::swift_dynamicCastFailure(swift::TargetMetadata<swift::InProcess> const*, swift::TargetMetadata<swift::InProcess> const*, char const*) + 106
        9   libswiftCore.dylib                  0x00007fff30c9a049 swift_dynamicCast + 249
        10  libswiftFoundation.dylib            0x00007fff599ae17a closure #2 (Swift.UnsafeMutableBufferPointer<A>, Swift.UnsafeMutableBufferPointer<B>) -> Swift.Int in static (extension in Foundation):Swift.Dictionary._forceBridgeFromObjectiveC(_: __C.NSDictionary, result: inout Swift.Optional<Swift.Dictionary<A, B>>) -> () + 762
        11  libswiftFoundation.dylib            0x00007fff599ae1f7 partial apply forwarder for closure #2 (Swift.UnsafeMutableBufferPointer<A>, Swift.UnsafeMutableBufferPointer<B>) -> Swift.Int in static (extension in Foundation):Swift.Dictionary._forceBridgeFromObjectiveC(_: __C.NSDictionary, result: inout Swift.Optional<Swift.Dictionary<A, B>>) -> () + 39
        12  libswiftFoundation.dylib            0x00007fff599b2403 Swift._NativeDictionary.init(_unsafeUninitializedCapacity: Swift.Int, allowingDuplicates: Swift.Bool, initializingWith: (Swift.UnsafeMutableBufferPointer<A>, Swift.UnsafeMutableBufferPointer<B>) -> Swift.Int) -> Swift._NativeDictionary<A, B> + 195
        13  libswiftFoundation.dylib            0x00007fff599acf90 static (extension in Foundation):Swift.Dictionary._unconditionallyBridgeFromObjectiveC(Swift.Optional<__C.NSDictionary>) -> Swift.Dictionary<A, B> + 448
        14  FirebaseStorage                     0x0000000102be888d @objc FirebaseStorage.StorageMetadata.customMetadata.setter : Swift.Optional<Swift.Dictionary<Swift.String, Swift.String>> + 93
        15  testing                             0x0000000101995c8a +[RNFBStorageCommon buildMetadataFromMap:] + 170
        16  testing                             0x00000001019970a2 -[RNFBStorageModule updateMetadata:::::] + 242

        17  CoreFoundation                      0x00007fff2040bfdc __invoking___ + 140
        18  CoreFoundation                      0x00007fff2040934f -[NSInvocation invoke] + 305
        19  CoreFoundation                      0x00007fff204095e2 -[NSInvocation invokeWithTarget:] + 70
        20  React                               0x00000001038bddb6 -[RCTModuleMethod invokeWithBridge:module:arguments:] + 2534
        21  React                               0x00000001038c227a facebook::react::invokeInner(RCTBridge*, RCTModuleData*, unsigned int, folly::dynamic const&, int, (anonymous namespace)::SchedulingContext) + 1402
        22  React                               0x00000001038c1b2c facebook::react::RCTNativeModule::invoke(unsigned int, folly::dynamic&&, int)::$_0::operator()() const + 156
        23  React                               0x00000001038c1a89 invocation function for block in facebook::react::RCTNativeModule::invoke(unsigned int, folly::dynamic&&, int) + 25
        24  DetoxSync                           0x0000000103586b27 ____detox_sync_dispatch_wrapper_block_invoke + 23
        25  libdispatch.dylib                   0x00007fff201148e4 _dispatch_call_block_and_release + 12
        26  libdispatch.dylib                   0x00007fff20115b25 _dispatch_client_callout + 8
        27  libdispatch.dylib                   0x00007fff2011c0df _dispatch_lane_serial_drain + 753
        28  libdispatch.dylib                   0x00007fff2011ccc1 _dispatch_lane_invoke + 400
        29  libdispatch.dylib                   0x00007fff2012797b _dispatch_workloop_worker_thread + 779
        30  libsystem_pthread.dylib             0x00007fff701c7fd0 _pthread_wqthread + 326
        31  libsystem_pthread.dylib             0x00007fff701c6f57 start_wqthread + 15
)

Steps to reproduce:

1- create a StorageReference with customMetadata = { removeMe: 'please'} 2- using that reference call updateMetadata({ removeMe: null }) 3- now the reference should have customMetadata with removeMe == undefined (that is, it should be been removed)

Relevant Code:

Here is our test that used to run okay on iOS (it had a failure on android we're still pursuing, but iOS was working)

https://github.com/invertase/react-native-firebase/blob/52715959a375e8babd129baec38356b78ce80ab6/packages/storage/e2e/StorageReference.e2e.js#L403-L428

    // FIXME not working against android on emulator? it returns the string 'null' for the cleared customMetadata value
    it('should set removed customMetadata properties to null', async function () {
      if (device.getPlatform() === 'ios') {
        const storageReference = firebase.storage().ref(WRITE_ONLY_NAME);
        const metadata = await storageReference.updateMetadata({
          contentType: 'text/plain',
          customMetadata: {
            removeMe: 'please',
          },
        });

        metadata.customMetadata.removeMe.should.equal('please');

        const metadataAfterRemove = await storageReference.updateMetadata({
          contentType: 'text/plain',
          customMetadata: {
            removeMe: null,
          },
        });

        // FIXME this is failing the part that fails
        should.equal(metadataAfterRemove.customMetadata.removeMe, undefined);
      } else {
        this.skip();
      }
    });

Note that the documentation for writable metadata properties indicates you may delete them passing the empty string: https://firebase.google.com/docs/storage/ios/file-metadata#update_file_metadata

On Android (and web) writable metadata is removed by passing null https://firebase.google.com/docs/storage/android/file-metadata#update_file_metadata https://firebase.google.com/docs/storage/web/file-metadata#update_file_metadata

No mention is made on how to remove customMetadata keys.

If you attempt sending an empty string in for a key with customMetadata, that just results in the value being the empty string, the value is not removed, so there may be something to think about for API consistency.

But in the past at least, sending in a null for a customMetadata key removed it fine.

Here's the iOS code that does the update:

https://github.com/invertase/react-native-firebase/blob/52715959a375e8babd129baec38356b78ce80ab6/packages/storage/ios/RNFBStorage/RNFBStorageModule.m#L145-L156

  FIRStorageReference *storageReference = [self getReferenceFromUrl:url app:firebaseApp];
  FIRStorageMetadata *storageMetadata = [RNFBStorageCommon buildMetadataFromMap:metadata];

  [storageReference
      updateMetadata:storageMetadata
          completion:^(FIRStorageMetadata *_Nullable metadata, NSError *_Nullable error) {
            if (error != nil) {
              [self promiseRejectStorageException:reject error:error];
            } else {
              resolve([RNFBStorageCommon metadataToDict:metadata]);
            }
          }];

...and the buildMetadataFromMap method is

https://github.com/invertase/react-native-firebase/blob/52715959a375e8babd129baec38356b78ce80ab6/packages/storage/ios/RNFBStorage/RNFBStorageCommon.m#L395-L399

+ (FIRStorageMetadata *)buildMetadataFromMap:(NSDictionary *)metadata {
  FIRStorageMetadata *storageMetadata = [[FIRStorageMetadata alloc] initWithDictionary:metadata];
  storageMetadata.customMetadata = [metadata[@"customMetadata"] mutableCopy];
  return storageMetadata;
}

That storageMetadata.customMetadata = line is the crash, when the FIRStorageMetadata is initialized with an NSDictionary that contains nulls

I understand that nil NSNull null etc can be bothersome in Objective-C and for interop, and who knows what the react-native bridge is translating 'null' from javascript in to at the objective-c layer, right? so I chucked this line in to the code:

  NSLog(@"FIRStorageMetadata removeMe key %@", metadata[@"customMetadata"]);

And it spits out this, which seems to indicate it is an NSNull if I understand correctly.

2022-05-26 17:23:08.876 Df testing[44279:500cc] FIRStorageMetadata removeMe key {
    removeMe = "<null>";
}

I tried changing the swfit StorageMetadata.swift file from this repo a bit, playing with type signatures etc but if my objective-c skills are lacking (they are) my Swift skills are almost non-existent, and making interoperable optional/nullable types is beyond me unfortunately

Help?

paulb777 commented 2 years ago

@mikehardy Sorry about the trouble and thanks for the detailed report.

I'll investigate tomorrow if no one beats me to it. :)

mikehardy commented 2 years ago

Thanks! Honestly, I'm curious (on the theme of objective-c / swift skills) to see how this works out, I'm sure to learn something

paulb777 commented 2 years ago

I'm able to reproduce the 9.x behavior change with the following test:

Screen Shot 2022-05-27 at 9 46 10 AM

My initial assessment is that even though this is an inadvertent API breakage, 9.x is actually fixing a bug. customMetadata is documented and implemented to be a Dictionary with String keys and String values. It's not valid to store an NSNull as a value and 8.x was only allowing it because of Objective-C's sloppy type handling.

customMetadata should be updated just like any other Swift or Objective-C Dictionary that specifies its key and value types. Is it feasible to update the react-native bridge accordingly?

cc: @tonyjhuang for any additional thoughts about this assessment or the Android/web comments in the OP.

mikehardy commented 2 years ago

I can do anything reasonable in react-native-firebase, so my direct question then is:

what exactly do I need to do in order to remove a key from customMetadata? I don't have the Objective-C to turn "customMetadata should be updated just like any other Swift or Objective-C Dictionary that specifies its key and value types" into a mental algorithm and then code. Do I need treat it as an "all at once" update, and if there is a null key, fetch current metadata so I have full set, remove keys with null values, then set the whole thing? Or ? :thinking:

mikehardy commented 2 years ago

(with time available / in absence of implementation direction I'll play around with the idea / hypothesis of "whole update without key removes the key" by using NSMutableDictionary's remove methods, or the filter methods etc + test what happens when I send in a customMetadata object with key missing to see what it looks like when it is refetched)

paulb777 commented 2 years ago

Yep, make a mutableCopy of the customMetadata and use removeObjectForKey: and then update it.

mikehardy commented 2 years ago

I'm more thoroughly verifying all metadata behavior now, and unfortunately it appears we were never verifying just removing regular metadata.

Setting it to null on android against storage emulator does not clear it, and I have yet to test ios behavior either v8 or v9. We have had metadata issues against the storage emulator that did not manifest in cloud storage before so I need to isolate whether it happens against cloud storage or just emulator but there's a wrinkle, besides the platform diparity where for settable metadata it's either set to null to clear (web, android), or set to empty string (ios, but what if you want the metadata to exist and just be empty? perhaps that is not valid for any of the settable ones)

I can attest that android right now does not purge entries from customMetadata that are not mentioned when you call updateMetadata, so if you have 2 keys and then send one in with null, android will return customMetadata with just the one you did not send. Stated differently: you send null to clear a key, and unsent keys are retained on android

So at minimum here for customMetadata on ios, correcting a bug with regard to sloppy nullness or not, we now have a platform difference between android and ios with regard to customMetadata key clearing, and I fear I may have to do a metadata fetch / local process / update metadata in order to coalesce the platforms.

I do understand that the use case of clearing keys on customMetadata was apparently never specified, I understand it was always either a null object or string/string map (in whatever language typing that is), but clearing via null did used to work :shrug: and I'm not sure how to clear a key idiomatically otherwise except in platform-specific ways now since android retains and we hypothesis for ios I need to clobber the whole map

paulb777 commented 2 years ago

I've confirmed the behavior in #9858 that customMetadata updates to the server as a full replacement dictionary.

@vkryachko or @tonyjhuang - Any ideas about why Android is different?

mikehardy commented 2 years ago

I haven't probed the JS or admin SDKs, not sure which side they fall on for customMetadata (null key removes key vs have to do full update), for what it's worth. And note that iOS v8 was on the null key removes key side ;-)

mikehardy commented 2 years ago

Some interesting (?) results here from firebase-ios-sdk. I have focused my testing today on iOS + cloud storage (to avoid the storage emulator permutations, and not clutter it with android chatter).

Here's what I've got - a total failure to update metadata, from what I can tell. I checked against v8 here and v9, same results.

Create a text file, with no metadata set, and you get this as a result of the creation:


2022-05-30 07:46:55.517 Df testing[32055:72ea6] STORAGE returned dictionary {
    bucket = "react-native-firebase-testing.appspot.com";
    contentDisposition = "inline; filename*=utf-8''file1.txt";
    contentEncoding = identity;
    contentType = "text/plain";
    generation = 1653951632228772;
    md5Hash = "LwOwNje/Fik3eT91bw8Vgw==";
    metageneration = 1;
    name = "playground/1653914800319/list/file1.txt";
    size = 6;
    timeCreated = "2022-05-30T23:00:32.293Z";
    updated = "2022-05-30T23:00:32.293Z";
}

Attempt to update that text file, sending in metadata that looks like this:


2022-05-30 07:47:06.151 Df testing[32055:72f09] STORAGE pre-update metadata {
    cacheControl = "no-store";
    contentDisposition = "Attachment; filename=example.html";
    contentEncoding = gzip;
    contentLanguage = es;
    contentType = "image/jpeg";
    customMetadata =     {
        hello = world;
    };
}

And the update call will succeed, giving you...this ?

022-05-30 07:47:07.070 Df testing[32055:72ea6] STORAGE post-update metadata FIRIMPLStorageMetadata 0x60000165bb60: {
    bucket = "react-native-firebase-testing.appspot.com";
    contentDisposition = "inline; filename*=utf-8''file1.txt";
    contentEncoding = identity;
    contentType = "text/plain";
    generation = 1653951632228772;
    md5Hash = "LwOwNje/Fik3eT91bw8Vgw==";
    metadata =     {
        hello = world;
    };
    metageneration = 2;
    name = "playground/1653914800319/list/file1.txt";
    size = 6;
    timeCreated = "2022-05-30T23:00:32.293Z";
    updated = "2022-05-30T23:00:44.094Z";
}

Attempt to remove the settable keys, per the android (not iOS!) docs, by sending in null (iOS says to send in '' / empty string):

2022-05-30 07:47:07.784 Df testing[32055:72f08] STORAGE pre-update metadata {
    cacheControl = "<null>";
    contentDisposition = "<null>";
    contentEncoding = "<null>";
    contentLanguage = "<null>";
    contentType = "<null>";
}

and get

2022-05-30 07:47:09.042 Df testing[32055:72ea6] STORAGE post-update metadata FIRIMPLStorageMetadata 0x600001674780: {
    bucket = "react-native-firebase-testing.appspot.com";
    contentEncoding = identity;
    generation = 1653951632228772;
    md5Hash = "LwOwNje/Fik3eT91bw8Vgw==";
    metadata =     {
        hello = world;
    };
    metageneration = 3;
    name = "playground/1653914800319/list/file1.txt";
    size = 6;
    timeCreated = "2022-05-30T23:00:32.293Z";
    updated = "2022-05-30T23:00:46.064Z";
}

I'm out of time for the day, but I think what this is really showing is one of:

1- my test harness is utterly broken and my API usage is incorrect / based on misunderstandings 2- the documentation is wrong and updateMetadata can't actually set metadata, even the ones that say they are settable 3- the ever-present option representing a surprising form of error I have not thought of yet

I'm not sure I can move the area of "storage.updateMetadata" forward more pushing from the react-native side with information, I think the internal test harness here could use an expansion where all of the updatable metadata bits are tested to see if you all reproduce.

I can see your swift integration test here and it looks like it should be probing these things: https://github.com/firebase/firebase-ios-sdk/blob/585b4c83dbeca3e25895ae2f0d2ed7056b3cac7b/FirebaseStorage/Tests/Integration/StorageIntegration.swift#L442

The objective c integration test does not seem to be probing things, but if it's derived from swift now, I'm not sure if that means anything.

You can see where I added the logging I included above, and how we are calling the API, in these lines in my commit here https://github.com/invertase/react-native-firebase/pull/6274/commits/8528aa382de7e696fcac28df876567fcf9b74f24#diff-a1ebeb5a270518df94ba9c1257fda0fce39ff3fb494b57224dff3399917b6f94R148-R156

paulb777 commented 2 years ago

@mikehardy Thanks for sharing your investigation and navigating through some confusing APIs.

We are still running the Objective C integration tests with the Swift implementation and the test to clear metadata is at https://github.com/firebase/firebase-ios-sdk/blob/master/FirebaseStorageInternal/Tests/Integration/FIRStorageIntegrationTests.m#L657. This test passes with both v8 and v9. It's using nil to do the clearing. Where do you see docs that say to send in an empty string?

mikehardy commented 2 years ago

I'll examine those tests and see if I can push this forward today, thanks

To your question: https://firebase.google.com/docs/storage/ios/file-metadata#update_file_metadata

You can delete writable metadata properties by passing the empty string:

paulb777 commented 2 years ago

Thanks. My testing shows that the empty string only works for a subset of the writable metadata properties.

The tests use "nil" instead and that seems to work. I've created the internal b/234471744 to update the docs.

mikehardy commented 2 years ago

Excellent! We were sending NSNull in there - as context that's just what the react-native type conversion maps javascript-layer 'null' into - when I took the results of initWithDictionary from that and manually compared/converted NSNull to nil I was able to delete all the writable items except I was unable to clear customMetadata entirely.

I examine your test and note that instead of setting it to nil, you set it to an empty dictionary:

https://github.com/firebase/firebase-ios-sdk/blob/1658c4051e742d88105a6b23ffb4f8fabb0c00f0/FirebaseStorageInternal/Tests/Integration/FIRStorageIntegrationTests.m#L664

Even if documentation is updated to indicate clearing a value is done with nil vs empty string, custom metadata will not fit that documentation

I'm still having a lot of difficulty updating - not removing - and I'm still trying to figure out why, perhaps it is more subtle type stuff (like nil vs NSNull) - but I'm working under the assumption it is something we are doing incorrectly in react-native-firebase since the tests seem thorough at your layer

mikehardy commented 2 years ago

Alright, I am just stumped. I have tried setting storageMetadata.customMetadata to NSNull, nil (as documented, or to be documented), @{} and [NSDictionary dictionary] (both analogs to what you do in the test fixture) and I just cannot get the thing to go to null as asserted here in the test here

https://github.com/firebase/firebase-ios-sdk/blob/1658c4051e742d88105a6b23ffb4f8fabb0c00f0/FirebaseStorageInternal/Tests/Integration/FIRStorageIntegrationTests.m#L619

I just cannot see the problem. One of those moments where simply hearing that "yes indeed, I've run this test and I know those lines have executed and I know that setting customMetadata to [NSDictionary dictionary] actually results in nil coming back" would be great so I know if I should continue trying anything/everything here or not.

NSLog of the object right before updateMetadata API call when it is set like in your test harness:

2022-05-31 16:13:34.122 Df testing[45797:5196d] STORAGE pre-update outgoing storageMetadata FIRIMPLStorageMetadata 0x6000024f9360: {
    metadata =     {
    };
}

And right after in completion handler:

2022-05-31 16:13:35.047 Df testing[45797:5171d] STORAGE post-update metadata FIRIMPLStorageMetadata 0x6000024f90e0: {
    bucket = "react-native-firebase-testing.appspot.com";
    contentEncoding = identity;
    generation = 1654031604618449;
    md5Hash = "LwOwNje/Fik3eT91bw8Vgw==";
    metadata =     {
        hello = world;
    };
    metageneration = 3;
    name = "playground/1654031558982/list/file1.txt";
    size = 6;
    timeCreated = "2022-05-31T21:13:24.686Z";
    updated = "2022-05-31T21:13:35.002Z";
}

:weary:

morganchen12 commented 2 years ago

The snippet in the docs is fixed in https://github.com/firebase/snippets-ios/pull/282, but looks like this issue should still remain open to address the custom metadata issue.

paulb777 commented 2 years ago

Confirmed locally and in #9870. For me, both

                        metadata.customMetadata = [NSDictionary dictionary];
                        metadata.customMetadata = nil;

clear the customMetadata on the server.

mikehardy commented 2 years ago

Thanks for that @paulb777 - no eureka moment but for me anyway, knowing someone else has seen it work gives me the will to carry on when otherwise stumped. I'll find the issue, and I bet it will be connected with why I can clear (most) data now but my updates aren't taking effect.

I think the remaining items are

the update all at once policy is a little uncomfortable for an implementer because we do not always have all of the metadata so now we need to fetch / process / remove keys as a facade for the ios platform, but it's technically possible at least

paulb777 commented 2 years ago

Hmm, based on testUpdateMetadata passing with both v8 and v9, I'm not sure anything on iOS has changed other than the NSNull issue discussed above.

The docs fix should be on the way.

morganchen12 commented 2 years ago

The docs fix was submitted yesterday (cl/452158983, for googlers).

paulb777 commented 2 years ago

I'm going to close, since not clear there's anything else for us to do here.

@mikehardy Thanks for the report and let us know if there's anything else.

mikehardy commented 2 years ago

Sorry this has hung out there - I'm still actively working on the task but you are right there may not be anything actionable for firebase-ios-sdk as it's conforming with published types now. Just firebase-android-sdk apparent behavior difference and react-native-firebase implementation shortcomings is what it looks like to me as well

mikehardy commented 2 years ago

It has something to do with using FIRStorageMetadata initWithDictionary or not

If I do what you do in the integration tests and just alloc/init then carry my data in "manually" in code the metadata values are updated correctly, but if I initWithDictionary it does not. Specifically it appears the contentEncoding and contentLanguage are not set to the correct type so the whole metadata update silently fails (an issue, I think? you can try to set your update-able metadata values into an NSDictionary, do an initWithDictionary on the StorageMetadata object using that dict and see it I think)

Took a while to isolate what the difference was here! Still not sure exactly why, but that's why react-native-firebase was not successfully updating metadata and your integration test is/was.

https://github.com/firebase/firebase-ios-sdk/blob/82483b6205423c3cc2f6bf4457663903b2962111/FirebaseStorageInternal/Sources/FIRStorageMetadata.m#L38-L43

Still working through this (finally got another time slice for it...) but that's a result worth reporting.

paulb777 commented 2 years ago

Thanks @mikehardy. I'll reopen to investigate.

mikehardy commented 2 years ago

apologies ;-)

A confounding factor: now I have a method for setting/updating metadata which is nice, but since I can't use initWithDictionary (for whatever reason) I have to manually set each element.

Subtle issue with that: StorageMetadata.md5Hash is settable on initial upload, but not on updates. So my metadata utility function needs to copy it in if it is there (in case it is the initial upload case) but it's readonly, so the compiler won't let me:

https://github.com/firebase/firebase-ios-sdk/blob/82483b6205423c3cc2f6bf4457663903b2962111/FirebaseStorageInternal/Sources/Public/FirebaseStorageInternal/FIRStorageMetadata.h#L65

It works in initWithDictionary because you're in class and can assign to _md5Hash

Exercising an API 100% is...illuminating :-)

mikehardy commented 2 years ago

Another fun trick: trying to clear out metadata now that I've got update understood better. If you alloc/init a blank FIRStorageMetadata then set the 5 update-possible properties to nil, then call updateMetadata on the ref with that metadata, it won't clear them out.

What's different between your integration test and my updateMetadata implementation? You're chaining off create/update calls before nil'ing them - it's not a clean / otherwise empty metadata object.

Try doing an alloc/init of a fresh FIRStorageMetadata here instead of using the response from previous update:

https://github.com/firebase/firebase-ios-sdk/blob/b85e5a66391b99712f6bac4de8eaf8694c2bc9fd/FirebaseStorageInternal/Tests/Integration/FIRStorageIntegrationTests.m#L658

I'm about done for the day but I'm going to attempt to implement the opposite - for update metadata I'm going to try calling metadataWithCompletion then do my updates in the completion handler using the returned metadata as my base instead of the empty/pure alloc/init

mikehardy commented 2 years ago

That was it! If on my updateMetadata API I first call fetchWithCompletion and then manually copy the 5 props in the completion handler into the obtained metadata object then pass that to update metdata you can update it and nil it out, everything works. So in order for nil-ing out those 5 properties to work it has to be based off an existing metadata for some reason, it cannot be a fresh alloc/init metadata.

  ✔ should return the updated metadata for a file (7119ms)

I think there is something worth investigating here as I would expect these experimentally observed things to not happen:

But it can be made to work and I've done so (at least on iOS against cloud storage), so I am not blocked on this. Just need to have the same success now against storage emulator and/or file issues there then make it work the same in android. No problem...

Thanks for the help here and elsewhere as ever @paulb777

paulb777 commented 2 years ago

@mikehardy Glad you got it working. 👍

I'm not sure I follow the point about initWithDictionary? See the added test in #9926

mikehardy commented 2 years ago

Well :thinking: - forgot about that test, sorry. My experimental observations against v8 were definitely showing something, and it was that initWithDictionary was not working for me. I wasn't setting md5hash https://github.com/firebase/firebase-ios-sdk/pull/9926/files#diff-4e116b2f1110087a3fd42b6088bff0b29f7afe50ad5685019bec66eee6ee1626R686 - maybe that's it ?

Not supposed to send md5hash in on an update I think, so I'm not

Also, this will fail if you go against the storage emulator in my experience, for some reason, while application/octet-stream will work - do you execute these against emulator or cloud? https://github.com/firebase/firebase-ios-sdk/pull/9926/files#diff-4e116b2f1110087a3fd42b6088bff0b29f7afe50ad5685019bec66eee6ee1626R683

paulb777 commented 2 years ago

Yep, needed to set md5hash for the assertMetadata validation. The contentEncoding updates are also kind of weird, but consistent between v8 and v9. These tests run against a real project in the cloud.

mikehardy commented 2 years ago

One last item here before I move on with current set of workarounds for whatever is going on here - it appears that StorageMetadata name / path are conflated in firebase-ios-sdk whereas firebase-js-sdk and firebase-android-sdk

Here name == path, despite documentation indicating it will be last path component - JS and Android are correctly returning just last path component as name

So in our test harness right now I've got this cutout for the platform difference

      const storageReference = firebase.storage().ref(`${PATH}/list/file1.txt`);
      const metadata = await storageReference.getMetadata();
      metadata.generation.should.be.a.String();
      metadata.fullPath.should.equal(`${PATH}/list/file1.txt`);
      if (device.getPlatform() === 'android') {
        metadata.name.should.equal('file1.txt');
      } else {
        // FIXME on ios file comes through as fully-qualified
        // TODO log issue with firebase-ios-sdk repository
        metadata.name.should.equal(`${PATH}/list/file1.txt`);
      }

shows up against cloud storage and storage emulator so I think it's got to be in here?

mikehardy commented 2 years ago

Apart from bug-spam - some gratitude! I just got react-native-firebase v15 out the door with firebase-ios-sdk v9 https://github.com/invertase/react-native-firebase/blob/main/CHANGELOG.md#1500-2022-06-20 - thanks for all the help

paulb777 commented 2 years ago

@mikehardy Congrats on the release of react-native-firebase v15! 🎆

I'm not able to reproduce the metadata name problem you describe. This new integration test works on a real Firebase project: https://github.com/firebase/firebase-ios-sdk/pull/9926/commits/018cc6b930f96b9b6254cf96db5d599c38accd09

paulb777 commented 2 years ago

Going to close now since no identified next steps, but feel free to continue the conversation here.