File::copyFileTo fails on iOS

I’m trying to debug an issue on iOS where a call to file.copyFileTo(newFile) fails.

jassert(newFile.hasWriteAccess()); passes.

The copyFile method:

bool File::copyFileTo (const File& newFile) const
{
    return (*this == newFile)
            || (exists() && newFile.deleteFile() && copyInternal (newFile));
}

I have confirmed that *this != newFile
The file does not exist.
deleteFile returns true.

The debugger does not take me into copyInternal at all (I have a breakpoint set there).

Any clues?

1 Like

I run into the same iOS problem. It works fine on MacOS.

Does anyone have any new information?

From where you’re trying to copy? are you using native FileChooser?
If you’re trying to copy a file outside of your app (eg from iCloud/Local Device/Files) you need to use Url and maybe even handle input/output streams yourself.

Yes, I’m using the native FileChooser.

void FileManager::openFileChooser(
                                  const juce::String& dialogBoxTitle,
                                  const juce::File& initialFileOrDirectory,
                                  const juce::String& filePatternsAllowed,
                                  bool useOSNativeDialogBox,
                                  bool treatFilePackagesAsDirectories,
                                  Component* parentComponent,
                                  int FileChooserFlags)
{
    fileChooser.reset (new juce::FileChooser (dialogBoxTitle, initialFileOrDirectory, filePatternsAllowed, useOSNativeDialogBox));

    fileChooser->launchAsync (juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectFiles,
                     [this] (const juce::FileChooser& chooser)
                     {
                        compoListeners.call([this](Listener &l)
                        {
                            l.fileChanged(this->fileChooser->getURLResult());
                        });
                     });
    
}

And here is the copy function:


void AppSoundControls::fileChanged (juce::URL url)
{
    juce::File selectedFile = url.getLocalFile();
    if(!selectedFile.exists())
    {
        DBG(__FUNCTION__ << " - No file selected " << selectedFile.getFullPathName());
        return;
    }
    DBG(__FUNCTION__ << " - Selected file exists: " << selectedFile.getFullPathName());
    
    const juce::File newFile = FileHelpers::getProjectsDir().getChildFile(processor.getProjectState().getProjectName()).getChildFile(Uuid().toString() + "_" + File::createLegalFileName(selectedFile.getFileName()));

    jassert(newFile.hasWriteAccess());  // No errors
    if(!selectedFile.copyFileTo(newFile))
    {
        DBG(__FUNCTION__ << " - Copy error: " << newFile.getFullPathName());
        return;
    }
    DBG(__FUNCTION__ << " - Copy success: " << newFile.getFullPathName());
}

As I said, this works on macOS.

Sadly on iOS especially with Files supporting ‘cloud’ documents you’ll need to use the InputStream and write it yourself.

I wish JUCE would have some nice async/threaded mechanism built-in. sadly currently there’s none.

So after you get the inputstream properly, write it to a fileoutputstream in your ‘sandboxed’ / app writable path(s).

1 Like

Thanks for your hint @ttg !

It works now on iOS and MacOS:

void AppSoundControls::fileChanged (juce::URL url)
{
    auto inputStream = url.createInputStream(false);
    if(inputStream == nullptr)
    {
        DBG(__FUNCTION__ << " - Error: Can't create inputStream ");
        return;
    }
    else
        DBG(__FUNCTION__ << " - Ok: inputStream created");
    
    juce::File newFile = FileHelpers::getProjectsDir().getChildFile(processor.getProjectState().getProjectName()).getChildFile(Uuid().toString() + "_" + File::createLegalFileName(url.getLocalFile().getFileName()));
    if(!newFile.hasWriteAccess())
    {
        DBG(__FUNCTION__ << " - Error: No write access ");
        return;
    }
    
    std::unique_ptr<juce::FileOutputStream> outputStream;
    outputStream = newFile.createOutputStream();
    outputStream->writeFromInputStream(*inputStream, inputStream->getTotalLength());
}