Problem/Motivation
Currently file_save_data depends on a number of services and calls logging and flash messages. We should deprecate it, and move it to a service so it can be better tested, throw exceptions and let calling code handle the logging and flash messages.
Steps to reproduce
Proposed resolution
Deprecate file_save_data and replace with a service.
Remaining tasks
User interface changes
API changes
file_save_data, file_move() and file_copy() are replaced with the FileRepository service and deprecated.
The new service has a new method called: loadByUri(string $uri) which loads the first File entity found with the specified URI.
Data model changes
Release notes snippet
file_save_data, file_move() and file_copy() are replaced with a service and deprecated.
The new service has a new method called: loadByUri(string $uri) which loads the first File entity found with the specified URI.
| Comment | File | Size | Author |
|---|---|---|---|
| #121 | interdiff-3223209-118-121.txt | 6.29 KB | yogeshmpawar |
| #121 | 3223209-121.patch | 71.39 KB | yogeshmpawar |
Comments
Comment #2
kim.pepperAdding related issues
Comment #3
kim.pepperHere's an initial patch...
Comment #4
kim.pepperFix coding standards.
Comment #6
daffie commentedOut of scope.
Why is this not done on a single line?
Out of scope.
This method might throw an \Drupal\Core\File\Exception\FileException only the new interface does not mention this.
Comment #7
berdirSame as in the system_retrieve_file() issue. We removed support for $destination = NULl when we deprecated lots of other unmanaged file API functions and should do it here too, this is an anti-pattern.
Interesting, was wondering why FileSystem::saveData() doesn't contain this check, but I think it's because we want to limit file entities to stream wrapper uris, while the lower-level APIs do support copying to any path.
See #2241865: Do not create a new file entity in order to overwrite an existing entity as a related issue that we haven't managed to get fixed in years. Not sure what to do. Updating the code to use that implementation might make it more complicated to land this, not doing it means we'll have to redo that patch.
That said, that also reminds me that this is not the only managed file API function. Should we really limit this service/issue to just file_save_data() or should we make a more generic FileSystemManaged or whatever we want to call it?
There's file_move(), file_copy() and... actually, that's it. The others are different scopes.. validate or upload related or hooks and small helper functions that aren't actually doing any IO.
Not sure how to name it, but I think it would be easier to do them all at once, then we don't need to juggle patches and constructor changes between different issues. And risk only getting some changes into the same minor version and need BC and stuff.
Thinking more about the name, FileEntityWriter or something in that direction might work, copy/move/save is all about saving files. We managed to almost entirely remove the term "unmanaged files" from core, just a few comment leftovers from what I see, so I think we should do the same with the term "managed files", aka _not_ use ManagedFileWriter or so.
Hm, I guess the hook stuff below prevents us from using expectsException()? Makes me wonder if that's really worth testing when we have an exception, can we assume that this will abort early? Not sure.
I guess you didn't quite finish here ;)
As you can see in the test fail, we have a hidden dependency on file.module here, so you need to put the \Drupal::service() call inline into the condition or make it an if/else.
Comment #8
kim.pepperThanks for the reviews.
@daffie re: #6
@Berdir re: #7
Comment #9
kim.pepper> Will be more careful in future!
🧐
Comment #10
kim.pepperAnother one!
Comment #12
kim.pepperAdded CR and fixed deprecation message.
Comment #13
daffie commentedThe patch looks good to me now.
The only problem that is left for me it naming of the new class:
I am not really happy with the name Drupal\file\FileEntityWriter. Not sure if just Drupal\file\FileEntity is better, then we are getting very close to Drupal\file\Entity\File. I am guessing that naming things in programming is hard. :-)
Could we do an inventory of what the new class is going to do. Is it only going to do what it is now doing or are we going to add more methods to it?
Comment #14
berdirI recommend to have FileEntity in there to separate from Drupal\Core\File, but not feeling too strongly about it. I would like to get rid of the "Managed" term as mentioned above.
Comment #15
daffie commentedCould we maybe change the method name to "write"? Or are we going to write something else in this class beside files?
Comment #16
berdirWas wondering about that too. maybe writeData()? We're not going to write something else else than files, but we might write/save from different sources. the managed system_retrieve_file() _could_ be a writeFromUrl method on this service too?
Comment #17
daffie commented+1 for "writeData()"
Comment #18
kim.pepperOk. Changed to FileEntity::writeData()
Comment #19
kim.pepperUpdated CR
Comment #20
daffie commentedAll code changes look good to me.
The function has been deprecated and testing for it has also been added.
For me it is RTBC.
Comment #21
berdirI'm not sure if we should move the move and copy functions in this issue as well. Doing it later might be more complicated for BC if we need to expand the interface, assuming we don't manage to do it before 9.3.
Comment #22
larowlanCan we add the issues for that to the related issues?
Comment #23
berdirI don't think we have issues yet for that. I guess I'm kind of asking whether we should do it here or create those issues :)
Comment #24
kim.pepperCreated followup:
Comment #25
larowlanIt would be good to get a rough idea of what the follow-up will look like before committing here, for the same reasons as @Berdir lists in #21
Comment #26
catchMoving back to needs review. If it makes the overall change simpler to review, then doing all three functions at once seems sensible, we can always split again later.
Comment #27
kim.pepperOK. This adds file_copy and file_move to the FileEntity service and deprecates them. Haven't updated the title or CR yet.
Comment #28
daffie commentedI am not sure if this can happen. If so, can you explain from were? If so, the function file_copy() does not throw this exception and it also does not catch it. Therefor something does not add up.
Same problem as with copy().
The docblock of the method copy() does not say anything about a FileExistsException.
Same problem as with copy().
You say that the contents changed and you are asserting that they are equal?
Same as with testCopy().
Comment #29
kim.pepperThanks @daffie
These tests are just the original tests to ensure we don't lose coverage.
Comment #30
daffie commentedFor #28.1: So they are missing from the docblock of file_copy(). Maybe we should add them.
For #28.2: So they are missing from the docblock of file_move(). Maybe we should add them.
For #28.3 and #28.4: Could we add a comment with that the exception FileExistsException is also a FileException.
I have just 1 point left:
I am missing testing for all those exceptions.
Comment #31
kim.pepperRe: #30
I refactored out some of the duplicate code in FileEntity. I think it makes it more readable now!
Comment #32
kim.pepperFix code sniff.
Comment #33
kim.pepperComment #34
kim.pepperUpdated CR
Comment #35
daffie commentedPatch looks good. Just one nitpick left for me. After that is it RTBC for me.
For both: can we do this on a single line.
By the way: adding the new 2 helper methods loadFirstByUri() and createOrUpdate() is a great idea!
Comment #36
kim.pepperFixes for #35
Comment #37
kim.pepperComment #38
daffie commentedAll code changes look good to me.
The functions are deprecated and have deprecation message testing.
The IS and the CR are in order.
The new service has testing added.
For me it is RTBC.
@kim.pepper: Great work!
Comment #39
berdirA bit of feedback on the new API, didn't look at the full patch. Crosspost with #38, didn't see the RTBC before I posted it, but I think we can improve things a bit more.
I know we have those exceptions, but they're all pretty bogus here and are just inherited from generic plugin managers. They would be thrown if the file entity type does not exist or is invalid, but it's almost impossible to get here without a valid file entity type definition, at least for copy/move since you are passing in a file entity already :)
With all that refactoring, I'd really prefer if we'd just fix that problem. Because this current architecture makes it in fact much harder to do so because you require that create or update receives a file entity already.
What if we pass in a URI instead and create the file entity only if no existing one was found?
And I'm thinking about making this method public and part of the API. Because then we can IMHO just document that for the system_retrieve_file() with a managed file use case (which is quite rare) that you just let it create an unmanaged file and then call createOrUpdate($uri).
I'm confused about the new interface/class name, initially I thought you moved those methods to the file entity class.
Why switch to FileEntity/FileEntityInterface? move/copy is IMHO still about writing/copying data, so the Writer part isn't wrong?
That said, this service is actually not really writing anything, we leave that to the underlying file system API. This really is about file storage operations.
We've been discussion about repository services for entitys for years, so why not go with that? FileRepositoryInterface.
"updating the file storage" sounds strange to me. IMHO "file storage" is the handler that is responsible for saving, we don't update that. Maybe just "when there is an error saving the file" (doesn't really sound like proper english to me just like now, but not sure)
Comment #40
kim.pepperThanks @Berdir!
createOrUpdate()method. The tests are passing, so I think its working correctly, but would be good for someone who knows that issue to review the changes.I also made
createOrUpdate()public and added to the interface.FileRespositoryis more appropriate. Renamed.Comment #41
kim.pepperShould
function loadFirstByUri(string $uri): ?FileInterfacebe public too? 🤔Comment #42
daffie commentedAs we make this method part of the public API, we should also add some testing for the method.
Nitpick: Could we add an empty line before the return statement.
For #41: To me it is a helper function, but feel free to disagree.
Comment #43
berdir> For #41: To me it is a helper function, but feel free to disagree.
FWIW, the idea of those repository services is exactly this: providing commonly used helpers (like user load by mail and so on for users) around entity crud operations. nothing that we do here is rocket science or impossible without this service.
Given that, I'd say why not? loading by uri is a pretty common use case for files. There are more such calls in file.module and other places, file_file_download() (although that is actually a loop, I think that's due to case sensitivity problems) and _file_save_upload_single().
Also just found #685818: Provide an API function to convert a URI to a file object, which we could close as a duplicate of the new public createOrUpdate() method. this exists in file_entity.module as file_uri_to_object().
Comment #44
daffie commentedIf you both think
loadFirstByUri()should be part of the public API, then lets do that!Comment #45
kim.pepperI don't think the method signature
public function createOrUpdate(string $uri, string $destination, int $replace = FileSystemInterface::EXISTS_RENAME): FileInterfaceis right. What does $destination or $replace mean in this method?It would make more sense if it were just
createOrUpdate(string $uri): FileInterface, but then we would need to re-save if we are doing any renaming.I think the code from #2241865: Do not create a new file entity in order to overwrite an existing entity is either incorrect, or I don't understand it.
Comment #46
kim.pepperChatting it through with @larowlan I think we need some changes here.
I've made a new public method on the interface
public function loadOrCreateByUri(string $uri): FileInterface. This better matches the #685818: Provide an API function to convert a URI to a file object. Added tests.I've changed
protected function createOrUpdate(string $uri, string $destination, int $replace = FileSystemInterface::EXISTS_RENAME): FileInterfaceback to protected, as I think it is only useful internally to this class.Re: #42
Re: #43 so now we have two new public methods on the interface:
public function loadOrCreateByUri(string $uri): FileInterfacepublic function loadByUri(string $uri): ?FileInterfaceComment #47
kim.pepperFix code sniff
Comment #48
kim.pepperFixes some variable names.
Comment #49
kim.pepperFix more variable names.
Comment #50
kim.pepperLast variable name. 🤞
Comment #51
kim.pepper@Berdir in #43:
Should we handle that here also??
Comment #52
berdirMaybe? I'm actually unsure about the current state of the case sensitive behavior with our different database backends and if that still is an issue or not.
Including the loop in that method would be a minimal overhead even if it's not strictly necessary anymore and we could use it in those methods too without having to spend a lot of time to investigate whether or not it's required?
I also know that the URI isn't actually enforced to be unique and people have actual duplicate managed file entries too, but that's a separate topic.
Comment #53
kim.pepperOK. I've added the check for case-sensitivity and replace a couple of usages in file.module and editory.module.
Comment #54
kim.pepperFix spelling. 🤔
Comment #55
daffie commentedPatch looks good. Just a couple of nitpicks:
Should the variable name not be: $file_repository.
Could we add or replace with: "@covers ::writeFile". Same for the other test methods in this class. It is better for the coverage report generator.
Could we add or replace with: "@coversDefaultClass \Drupal\file\FileRepository". Same for the test method with "@covers ::copy".
Could we add or replace with: "@coversDefaultClass \Drupal\file\FileRepository". Same for the test method with "@covers ::move".
Comment #56
kim.pepperFixes for #55
Comment #57
daffie commentedAll code changes look good to me.
The 3 functions are deprecated with deprecation message testing.
The new methods have testing.
The IS and the CR are i order.
For me it is RTBC.
@kim.pepper: Great work.
Comment #58
larowlanAdding review credits for daffie and berdir
The momentum on this issue has been amazing, awesome work folks
Will try to review next week
Comment #59
larowlanThis is looking great, just a couple of minor questions/observations from first quick pass
Did we always throw this and are now just documenting it - or is this an API change?
The change notices says file.entity, but it's file.repository now, so we need to update the change record
In once case we use clone, but in the other we use ::createDuplicate
Should we be using ::createDuplicate everywhere?
still using the old name here in the comment too
here too
Comment #60
berdir1. It's not new. We would have to add throws EntityStorageException to 50% of our API, because anything that saves an entity could theoretically do that. And we do that a lot ;)
3. One is a copy, so a new file entity, the other is move. I think the clone on move is done to not change $source, in case you want to compare against that (did filename change or something like that), so I think that makes sense.
Need work for the comment updates :)
Also, in regards to credits, not sure what you want to do about the related issues that this is resolving:
* #685818: Provide an API function to convert a URI to a file object
* #2241865: Do not create a new file entity in order to overwrite an existing entity
Both have been open for many years and had plenty of people contributing over that time.
Comment #61
larowlanYeah we should add those folks here if they're likely to be closed as duplicates.
Comment #62
kim.pepper#59 Updates comments and CR.
Comment #63
daffie commentedAll points of @larowlan have been addressed.
Back to RTBC.
Comment #75
larowlanComment #76
larowlancross-post - added credit for those issues and marked them as duplicates
Comment #77
kim.pepperThe 10yr old #1048114: file_copy(), $destination cannot be NULL will also be resolved with this issue.
Comment #78
larowlanAre you able to transfer the credit over here and mark that as duplicated? Thanks
Comment #79
brianV commentedLarowlan - looks like you mistakenly credited me for this one instead of Berdir. I'm not sure how to change the credit system, but want to make sure credit goes where credit is due!
Comment #83
phenaproximaTransferring credit from #1048114: file_copy(), $destination cannot be NULL per #77.
Comment #84
phenaproximaRemoving @brianV credit per #79.
Comment #85
berdir@brianV: That was not a mistake. You've been credited because you've worked on the now-duplicate #685818: Provide an API function to convert a URI to a file object. Almost 10 years ago :)
While you didn't directly contribute to this issue, we try to take over credits in such a case as you would have been created if that issue would have been committed/fixed on its own.
Comment #86
larowlanWent over this more thoroughly, a few minor nits are all that remain, and one question about PHP version support for the tests
old name here
the 8 here isn't needed, its the default
rename is the default, so not needed here
assertFileDoesNotExist seems to be PHPUnit 9+ - does that cause issues for getting this passing on lower versions of PHP? We should be able to queue up a lower version of PHP to check.
Comment #87
kim.pepperFixes for all of #86
Comment #88
andypostLooks all points addressed, back to RTBC (hope CI will be green)
Comment #89
andypostGreen and ready
Comment #90
alexpottNeed a reroll of the patch - there have been a couple of changes to file.module since the patch was made.
Comment #91
alexpottThe only usage of this is in the very similarly named and protected
createOrUpdateon this class. Why are we adding this here and how is this additional API justified? Oh I see we're trying solve a twelve year old issue in a refactor #685818: Provide an API function to convert a URI to a file object. I'm not sure this is a great idea. Firstly, because we adding new API in a refactor so the new API never gets the discussion it deserves. Secondly, because while functions that load or create always seem like a good idea at the start they often seem to back us into complex corners. For example, with Config objects you need to check the isNew() more often than you'd like. With the function added here - if the file doesn't exist then the caller is responsible for calling ->save() but if it does exist then you shouldn't call save because that'd result in unnecessary writes to the database.Comment #92
kim.pepperLet's start with the re-roll...
Comment #93
kim.pepperRe: #91 I'd be happy to remove
loadOrCreateByUri()off the interface and inline it into the protectedcreateOrUpdate()method. We would still be fixing the long-standing bug, but would then be able to address the API in a follow up (if we want). Would that work?Here's a patch that does that.
Comment #94
daffie commentedThe method loadOrCreateByUri() has been removed from the patch.
I have updated the CR and the IS.
Back to RTBC.
Comment #95
berdirHm.
I'd suggest that if we don't do it here and now then we just won't fix the other issue. we have loadByUri() now, and it's fairly simple then. Adding it later to this repository isn't work the BC dance that it would require.
That said, I'm wondering where the permanent call here is coming from, loadOrCreate didn't do that? If you move a temporary file that shouldn't automatically become permanent?
Also, not sure if the setOwner is necessary. We have the default value callback now on file entities, that should automatically happen.
Comment #96
berdirComment #97
kim.pepperAre you assuming
createOrUpdate()gets called frommove()?. It is only ever called fromwriteData().This test for this was copied from https://git.drupalcode.org/project/drupal/-/blob/9.2.x/core/modules/file.... So I assume we are keeping the existing behaviour?
I have moved the logic around a bit so it makes more sense.
Comment #98
kim.pepperWe discussed naming things in Slack. I've copied here for those watching, and to hopefully push this issue to a consensus.
So we are planning on adding
writeFromUrl()in future then we should call itwriteFromData()?Comment #99
kim.pepperOr
createFromData()andcreateFromUrl()?Comment #100
kim.pepper#3232248: Move _file_save_upload_single to a service and deprecate will be able to use the
loadByUri()method.Comment #101
kim.pepperInterestingly that ^ issue has a
createFromUpload()method, so maybecreateFromData()andcreateFromUri()is the right way to go.Comment #102
larowlan+1 to those names - but lets get a second +1 before reworking it
Comment #103
kim.pepperAnyone got a view either way?
Comment #104
kim.pepperBumping this as it looks like it's just agreeing on naming conventions. The rest of the feedback has been addressed.
Comment #105
andypostI think it needs @Berdir as current names works for me
I bet it will need DI
Comment #106
kim.pepperRe: #105.2 I couldn't find any subclasses of FileItemBase that call
__construct(). This might be because the constructor has 2 optional parameters name and $parent which are not used for dependency injection.\Drupal\Core\TypedData\TypedDataManagerInterface::create()does not allow you to pass in additional dependencies.We should probably leave this as is.
Comment #107
andypostI think it's ready for commiter's eyes
Thanks @larowlan who pointed that static method cant use injections - re #105.2
Comment #108
dwwI'm reviewing this now. Found some problems. I'll post the full comment and a patch shortly.
Comment #109
dwwThis mostly looks really great. Thanks to everyone who worked on it so far, especially @kim.pepper! Most of what I could find are docs nitpicks, fixed with the attached patch. The unaddressed points are:
Everything else is now fixed or informational. But I'll ping @kim.pepper in Slack to sort out 6.
Thanks!
-Derek
Why is this talking about file_copy() when we're inside file_move()?
A. Why is the text of this message changing? In particular, why is the log message from file_move() now talking about file_copy()?
B. Is there anything other than the lack of "(%realpath)" we can/should say differently in this case?
C. If we're changing the text of this for valid reasons, do we want to start wrapping the filename in double quotes (like we're tending to do with new messages in other issues)?
Whoops. Now "FileRepository".
Should these say "Invalid destination stream wrapper" or something? Mention "destination"? We already know it's an InvalidStreamWrapperException, seems the text could be more helpful than repeating that?
Why the | here? Why not just FileInterface and be done?
If EXISTS_ERROR causes this to return FALSE, we don't say that's allowed by docs or return type.
Doesn't look like we actually handle EXISTS_ERROR in createOrUpdate() at all. Maybe it doesn't matter, since we currently only call it in a method that would have already thrown the exception. But it seems a little fishy to document that as a valid choice but then not handle it. Should we throw the exception ourselves here?
=== ?
This interface does more than that, now. Can we do better? How about:
"Performs file system operations and updates database records accordingly."
?
If EXISTS_ERROR causes this to return FALSE, we don't say that's allowed by docs or return type.
It looks like the end result will be a
FileExistsExceptionthrown by FileSystem::prepareDestination():We should probably also mention FileExistsException in this list for each of the methods in this interface...
Same here. Docs are off. FileSystem::copy() calls prepareDirectory() which will throw the FileExistsException for us in this case, so nothing else to fix other than the docs.
Same here. Docs are off. In this case, FileSystem::move() calls prepareDirectory() which will throw the FileExistsException for us...
Did we lose this test coverage? I don't see what happened to it.
I suppose since it's testing an edge case of a now deprecated API, better to remove the coverage than complicate the new API to support this feature. I suppose we can live without testing file_save_data() between 9.3.0 and 10.0.0...
🎉 We've even got test coverage for this exception being thrown. So all we need are the doc updates mentioned above. 😉
Maybe mention this is also testing file_save_data() with no $destination set?
Oh slick, there it went! Never mind. 😉
Comment #110
andypostjust nit while skimed
The
%realpathis missing in message testComment #111
dwwre: #110: Good point. Fixed here.
Comment #112
kim.pepperRe: #109.6 This is an attempt to keep the signatures inline with the other methods. The only value that affects logic is EXISTS_RENAME.
I see two options:
$rename = FALSEand move the check to calling code.I think we should go with option 1. (see patch)
Comment #113
dwwCool, thanks. I think option 1 is more clear and explicit, and less weird. Even if it's slightly more hassle at the call site. But only a little. ;)
However:
Doc says optional, but function signature doesn't provide a default value for $rename. I assume we want to default to FALSE? We should make that true both in the signature, and in the docs.
Comment #114
dwwIn Slack, I proposed to @kim.pepper that we just drop "(optional)" from the docs and leave it as a required param. He agreed that was best, so here we go.
Comment #115
dwwRe-roll after #3232248: Move _file_save_upload_single to a service and deprecate landed. Interdiff is wildly confused, so I won't bother. We should probably be using an MR for this now, to make such operations more straightforward. /shrug But maybe this is the last re-roll we need? 🤞
Comment #116
kim.pepperI've worked on this too much to rtbc but RTBC +1 on green.
Comment #117
dwwArgh, nope, that's not right. The rename from SaveDataTest to LegacyFileTest conflicted with the new LegacyFileTest from #3232248: Move _file_save_upload_single to a service and deprecate and I didn't notice how broken that all was. I'm out of time right now, so I can't do this properly tonight. I'll be traveling all day tomorrow, and probably won't be able to do it. If @kim.pepper doesn't beat me to it, I'll try late tmrw or Friday. Sorry!
Comment #118
kim.pepperRe-roll of #114
Comment #119
daffie commentedIt all looks good to me. Just a couple of nitpicks:
Nitpick: Insert a space between
@file_usage',and'@current_userNitpick: Could we list this possible value first as it is the default value (3 times).
Nitpick: This change is out of scope.
Nitpick: This space at the end of the test method can be removed. The same for testCopy().
We can remove the third parameter. It is the default value.
Comment #120
yogeshmpawarWorking on #119 suggestions.
Comment #121
yogeshmpawarComment #122
kim.pepperCreated a followup #3245249: Replace FileUploadHandler::loadByUri() with FileRepositoryInterface::loadByUri(). I think there are potentially conflicting issues to do with FileUploadHandler right now, so I think this can be done as a follow-up.
Comment #123
daffie commentedAll the code changes look good to me.
The IS and CR are in order.
For me it is RTBC.
Comment #124
dww+1 RTBC from me. #118 is the correct re-roll that I botched in my haste in #115. #119 are legit points, all fixed by #121. I don't see anything else to complain about.
Also happy to report there's only a tiny, easily resolved conflict in
file.services.ymlbetween this and the arguably much more important #2940383: [META] Unify file upload logic of REST and JSON:API, so committing this will not slow us down over there.Thanks!
-Derek
Comment #125
larowlanSaving issue credits
Comment #126
larowlanDidn't mean to change status
Comment #127
kim.pepperComment #128
kim.pepperAdded release not snippet
Comment #129
kim.pepperTrimmed down the release note snippet
Comment #130
kim.pepperMore release not trimming
Comment #132
larowlanTagging for release notes
Committed 6407407 and pushed to 9.3.x. Thanks!
Published the change record.
Awesome work here folks, across a number of issues.
Comment #133
quietone commentedIn #132 is says 'Tagging for release notes' and it looks like that was missed. Adding tag.
Comment #135
solideogloria commentedUpdating to Drupal 9.3 breaks for
file_save_dataif the first parameter was passing aresourceinstead of astring. This was allowed and worked in Drupal 9.2, butFileRepository->writeData()doesn't allow a file handle as the data param, so I cannot use that...Also, while trying to port my code to Drupal 9.3, I cannot seem to figure out how to programmatically create a file entity because of this error.
Before:
After...?
Comment #136
kim.pepperHmm. I think that was an undocumented feature. The docblock specifies string.
Comment #137
berdirAgreed, that was not intentionally supported and not tested. Somewhat unfortunate, but you will have to either read the file content with file_get_contents() or write your own function to create a file entity.
Comment #138
solideogloria commentedOkay, thanks.
Here's what I went with, in case anyone else has the same issue. I figured that if support for a
resourcecould be removed from that function, then maybe it won't always be supported for$this->fileSystem->saveData(), as the docblock there saysstringas well. So I used$this->fileSystem->copy()and check if it's readable first, instead of opening the file.Comment #139
kim.pepperThat looks like a better way to do it.