В некоторых загруженных файлах отсутствуют байты (Apache Commons FTPClient)

Это продолжение моего другого вопроса, потому что я заметил, что другие типы файлов тоже затронуты (не только изображения).

Мое приложение для Android (Java 8, минимальный SDK 24, целевой SDK 27) загружает изображения (jpg, png) и текстовые файлы (txt) с FTP-сервера, используя Apache Common FTPClient (версия 3.6).

Проблема:

В определенном проценте этих файлов отсутствуют байты, и это также кажется довольно случайным:

  1. Если я загружаю файлы с сервера Win 10 в той же сети, около 50% файлов отсутствуют до 30 байт (изображения, для текстовых файлов обычно 1-2 байта).
  2. Если я загружаю файлы с сервера Linux в той же сети, обычно затрагиваются только 1-2 файла (из 10), и даже в затронутых изображениях обычно отсутствует только 1-2 байта. Редко никакие файлы не «сбоят».

Текстовые файлы, кажется, не сильно затронуты отсутствующими байтами, но изображения тем более. В зависимости от того, сколько байтов пропущено в итоге, они могут быть заметно повреждены: от отсутствующих/прозрачных строк внизу изображения (1-2 байта) до полностью перепутанных цветов или артефактов в нижней половине изображения (> 2 байта не хватает).

Я также протестировал свой код с двумя другими серверами, которые не находятся в одной сети:

  1. Частный сервер (не знаю, Win или Linux): он действительно глючит, и соединение часто просто обрывается при загрузке файлов, плюс он автоматически удаляет файлы раз в час, поэтому я не смог провести много тестов, но пока никакие загруженные файлы не были затронуты.
  2. DLP-тест: загруженные файлы удаляются через 15 или 30 минут, поэтому я не могу долгие тесты, но пока со скачанными файлами все в порядке.

Для тестирования я создал совершенно свежий проект. Я использую изображения, которые я создал сам (извините, я не могу ими поделиться) и случайные изображения, которые я нашел в Google — они весят от 12 КБ (которые всегда загружаются полностью) до около 8 МБ каждое. Вы можете найти альбом этих изображений (и поврежденных образцов загрузки) здесь. Файлы txt, которые я создал сам (извините, я тоже не могу ими поделиться), имеют размер около 14 МБ каждый.

Мое подозрение: файлы загружаются намного быстрее из той же сети, чем с внешнего сервера, поэтому, возможно, более быстрая загрузка вызывает «икоту», которая пропускает пару байтов. Но тогда почему это повлияет только на те, что в конце?

Мой код (без обработки исключений, так как это было бы слишком долго):

private boolean login() {
    try {
        ftpClient.connect(url,port);
        if(ftpClient.isConnected()) {
            if(ftpClient.login(username,password)) {
                return true;
            }
        }
    } catch (IOException e) {
        //Ignoring this for now
    }
    return false;
}

private void logout() {
    if(ftpClient!=null && ftpClient.isConnected()) {
        try {
            ftpClient.disconnect();
        } catch (IOException e) {
            //Ignoring this for now
        }
    }
}

public void downloadFiles(final String remoteFolder, final String localFolder, final ArrayList<String> filenames) {
    @SuppressLint("StaticFieldLeak") AsyncTask asyncTask = new AsyncTask<Object, Void, Void>() {
        @Override
        protected Void doInBackground(Object... params) {
            if(login()) {
                //Comment for version 1:
                //In my actual code (with error handling) I create the OutputStream 
                //object here, so I can close it in "finally" but I still open a new 
                //one for every file:
                //FileOutputStream out = null;
                try {
                    ftpClient.enterLocalPassiveMode();
                    ftpClient.setFileType(FTP.BINARY_FILE_TYPE);

                    if(ftpClient.changeWorkingDirectory(remoteFolder)) {
                        for (String filename : filenames) {
                            FTPFile[] singleFile = ftpClient.listFiles(filename);

                            if (singleFile != null && singleFile.length > 0) { //check if the file exists
                                String localPath = localFolder + File.separator + filename;

                                /////////////// Version 1 ///////////////
                                FileOutputStream out = new FileOutputStream(localPath);

                                if (!ftpClient.retrieveFile(filename, out)) {
                                    out.close();
                                    break;
                                }

                                out.close();

                                //Only for version 1:
                                File ff = new File(localPath);
                                long remoteLength = singleFile[0].getSize();
                                long newLength = ff.length();

                                if(newLength<remoteLength) {
                                    Log.d(TAG,filename+" - should be: "+remoteLength+" / is: "+newLength);
                                } else {
                                    Log.d(TAG,filename+" - OKAY");
                                }

                                //For version 2 & 3:
                                /*FTPFile single = singleFile[0];
                                long remoteLength = single.getSize();
                                InputStream is = ftpClient.retrieveFileStream(filename);
                                int len;
                                byte[] buffer = new byte[(int) (remoteLength)];
                                int newLength = 0;

                                /////////////// Version 2 ///////////////
                                BufferedInputStream bis = new BufferedInputStream(is);

                                while ((len = bis.read(buffer)) >= 0) {
                                    if(len>0) {
                                        newLength += len;
                                        out.write(buffer,0,len);
                                    } else {
                                        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500));
                                    }
                                }
                                bis.close();

                                /////////////// Version 3 ///////////////
                                ByteArrayOutputStream baos = new ByteArrayOutputStream();

                                while ((len = is.read(buffer, 0, buffer.length)) != -1) {
                                    baos.write(buffer, 0, len);
                                    newLength += len;
                                }

                                baos.flush();

                                FileOutputStream fos = new FileOutputStream(localPath);
                                fos.write(baos.toByteArray());
                                fos.flush();
                                fos.close();
                                is.close();

                                //For version 2 & 3:    
                                ftpClient.completePendingCommand();

                                if(newLength<remoteLength) {
                                    Log.d(TAG,filename+" - should be: "+remoteLength+" / is: "+newLength);
                                } else {
                                    Log.d(TAG,filename+" - OKAY");
                                }*/
                            } else {
                                break;
                            }
                        }
                    }
                } catch (IOException e) {
                    //Ignoring this for now
                }
            }
            return null;
        }

        @Override
        protected void onPostExecute(Void param) {
            logout();
        }
    };
    asyncTask.execute();
}

В моей версии с полной обработкой исключений нет никаких исключений/ошибок.

Версия 1, по-видимому, имеет самый высокий уровень успеха (около 1/20 файлов отсутствуют байты), и, похоже, нет разницы между версией 2 и версией 3.

Я заметил одну вещь: чем меньше buffer (версии 2 и 3), тем больше файлов полностью загружается с сервера 1. Размером, например, . 1000 только в 1-3 файлах не хватает байтов, но с 100000 это примерно половина "сбоя". Однако размер буфера, похоже, не сильно влияет на сервер 2, или, по крайней мере, это незаметно, потому что вероятность успеха уже довольно высока.

Вопрос:

Я что-то упустил в своем коде или я делаю что-то не так? Что может быть причиной того, что байты просто не «загружаются»? Следовательно: что я могу сделать, чтобы решить эту проблему?


person Neph    schedule 09.06.2020    source источник
comment
Пожалуйста, начните с использования более стандартного кода загрузки для своих тестов. См. Как загрузить файл с FTP с помощью Java?.   -  person Martin Prikryl    schedule 09.06.2020
comment
Итак, версия 1 с FileOutputStream вместо OutputStream (ответ AndroidTeam)?   -  person Neph    schedule 09.06.2020
comment
Да, FTPClient.retrieveFile с FileOutputStream.   -  person Martin Prikryl    schedule 09.06.2020
comment
Хорошо, только что сделал: первый запуск на сервере № 1 прошел идеально, второй на сервере № 2 дал мне 3 испорченных файла (из 13): 2 png (то же изображение, но другое имя) и файл txt отсутствует 1 байт каждый. Извините, забыл написать, что между кодом выше и моим есть одно отличие: у меня FileOutputStream out = null; находится в строке после if(login()), поэтому я могу закрыть его в своем finally (если нужно), но я все равно открываю новый для каждого файла, так что, надеюсь, это не должно иметь большого значения (я добавил это в код выше).   -  person Neph    schedule 09.06.2020
comment
Пожалуйста, отредактируйте свой вопрос, чтобы показать простейший возможный код, который воспроизводит проблему.   -  person Martin Prikryl    schedule 09.06.2020
comment
Не используйте промежуточный ByteArrayOutputStream. Вы можете записать байты непосредственно в FileOutputStream.   -  person blackapps    schedule 09.06.2020
comment
Я не вижу, чтобы вы регистрировали и сравнивали значения remoteLength и newLength. Вы также не сообщили нам.   -  person blackapps    schedule 09.06.2020
comment
Действительно, не делайте этот буфер размером с remoteLength.   -  person blackapps    schedule 09.06.2020
comment
@MartinPrikryl Это версия 1, остальное закомментировано.   -  person Neph    schedule 10.06.2020
comment
@blackapps Комментарий к объявлению 1: Использование ByteArrayOutputStream — это только одна версия. Их 3, и все они в какой-то момент выдают ошибку. Если вы хотите протестировать его, выберите любой, который вам нравится, но я бы рекомендовал версию 2/3, чтобы быстрее увидеть ошибку. ;) Комментарий к объявлению 2: Внизу есть Log.d для версии 2/3, и я только что добавил еще один для версии 1. Nor did you inform us - Что ты имеешь в виду? Комментарий к объявлению 3: Сделать размер буфера remoteLength — хороший способ проверить, сохраняется ли проблема. Тот факт, что вероятность успеха с меньшим размером составляет 90%, не делает его рабочим решением, на которое я могу полагаться/использовать.   -  person Neph    schedule 10.06.2020
comment
@blackapps Дополнение к комментарию 1: В самом начале версия 2 (та, которая записывает напрямую в FileOutputStream) использовала только InputStream вместо этого + BufferedInputStream, но я добавил последний, когда заметил проблему, пытаясь исправить это с помощью этого. Хотя, похоже, это не имеет значения.   -  person Neph    schedule 10.06.2020
comment
@blackapps Сравнение версий 2 и 3 было с самого начала, и я добавил код для версии 1 менее часа назад. Nor did you inform us. - О чем я вам не сообщил?   -  person Neph    schedule 10.06.2020
comment
Вы не сообщили нам, стала ли newLength равной remoteLength. Или если разница была уже и там эти два байта.   -  person blackapps    schedule 10.06.2020
comment
На данный момент имеет значение только то, что новый файл меньше файла на сервере (= исходный файл), потому что только тогда возникает проблема. Мне никогда не давали новый файл большего размера, чем исходный, поэтому я объединил этот случай с тем же размером. Кроме того, удаленный файл имеет только ограниченное количество байтов, которые он может пройти в цикле, поэтому локальный файл в любом случае не должен быть больше исходного. Я проверил размер файлов на сервере после их загрузки, и он такой же, как и у оригинала, поэтому что-то в процессе загрузки должно отбрасывать недостающие байты.   -  person Neph    schedule 10.06.2020
comment
Между прочим, разница между newLength и remoteLength не всегда равна 2. Иногда отсутствует 0 байтов, иногда 1, а иногда может быть 20. Кажется, довольно случайно, какие файлы затронуты и сколько байтов отсутствует. Я только заметил, что определенные версии моего кода в сочетании с определенными серверами чаще вызывают проблему, чем другие. Я написал об этом в своем вопросе и поделился всеми своими выводами.   -  person Neph    schedule 10.06.2020
comment
Вы тестировали загрузку с помощью клиента (GUI/командной строки), работающего на том же компьютере, что и ваш Java-код?   -  person Martin Prikryl    schedule 11.06.2020
comment
@MartinPrikryl Извините за поздний ответ! Вы имеете в виду, использовать что-то вроде Filezilla для загрузки файлов с сервера 1 или 2 на тот же компьютер, на котором я разрабатываю приложение для Android? Да, я использовал это приложение для загрузки файлов на серверы, а также только что проверил загрузку файлов несколько раз: все выглядит нормально, никаких странных артефактов и пропущенных байтов.   -  person Neph    schedule 15.06.2020
comment
Любая идея/предложение, что еще я мог бы попытаться решить эту проблему?   -  person Neph    schedule 17.06.2020
comment
Сегодня я протестировал тот же код в режиме ASCII, и в загруженных файлах тоже отсутствуют байты (около 66% успеха с сервером 1).   -  person Neph    schedule 23.06.2020
comment
Обновление: я попытался заменить AsyncTask на обычный Thread, но проблема осталась с первыми двумя серверами. Затем я написал свою собственную небольшую библиотеку для входа/выхода из системы и загрузки, используя Java по умолчанию Socket (поэтому без использования библиотеки Apache), но с этим тоже не повезло. Я также запустил свою библиотеку (но с SwingWorker вместо AsyncTask) как обычное приложение Windows с JFrame и JButton и протестировал ее примерно 30 раз с разными файлами: ни в одном из них не было пропущено ни одного байта. Похоже, это проблема с Android, сокетами и серверами в одной сети. :/   -  person Neph    schedule 02.07.2020