Подтвердить что ты не робот

Как избежать загрузки данных кэшированных приложений с Google Диска

В настоящее время я использую Google Android API Android для хранения данных своих Android-приложений в папку приложений Google Диска.

Это то, что я делаю при сохранении данных своих приложений

  • Создайте контрольную сумму для текущего локального zip файла.
  • Искать в папке приложения Google Диска, чтобы узнать, существует ли существующий zip файл приложения.
  • Если есть, перезапишите содержимое существующего zip файла App Folder с текущими локальными zip файлами. Кроме того, мы переименуем существующее имя файла zip файла App Folder с последней контрольной суммой.
  • Если нет существующего zip файла App Folder, сгенерируйте новый zip файл App Folder с локальным содержимым zip файла. Мы будем использовать последнюю контрольную сумму как имя файла ZIP-папки приложения Folder.

Здесь код, который выполняет вышеупомянутые операции.

Создать новый zip файл папки приложения или обновить существующий zip файл папки приложения

public static boolean saveToGoogleDrive(GoogleApiClient googleApiClient, File file, HandleStatusable h, PublishProgressable p) {
    // Should we new or replace?

    GoogleCloudFile googleCloudFile = searchFromGoogleDrive(googleApiClient, h, p);

    try {
        p.publishProgress(JStockApplication.instance().getString(R.string.uploading));

        final long checksum = org.yccheok.jstock.gui.Utils.getChecksum(file);
        final long date = new Date().getTime();
        final int version = org.yccheok.jstock.gui.Utils.getCloudFileVersionID();
        final String title = getGoogleDriveTitle(checksum, date, version);

        DriveContents driveContents;
        DriveFile driveFile = null;

        if (googleCloudFile == null) {
            DriveApi.DriveContentsResult driveContentsResult = Drive.DriveApi.newDriveContents(googleApiClient).await();

            if (driveContentsResult == null) {
                return false;
            }

            Status status = driveContentsResult.getStatus();
            if (!status.isSuccess()) {
                h.handleStatus(status);
                return false;
            }

            driveContents = driveContentsResult.getDriveContents();

        } else {
            driveFile = googleCloudFile.metadata.getDriveId().asDriveFile();
            DriveApi.DriveContentsResult driveContentsResult = driveFile.open(googleApiClient, DriveFile.MODE_WRITE_ONLY, null).await();

            if (driveContentsResult == null) {
                return false;
            }

            Status status = driveContentsResult.getStatus();
            if (!status.isSuccess()) {
                h.handleStatus(status);
                return false;
            }

            driveContents = driveContentsResult.getDriveContents();
        }

        OutputStream outputStream = driveContents.getOutputStream();
        InputStream inputStream = null;

        byte[] buf = new byte[8192];

        try {
            inputStream = new FileInputStream(file);
            int c;

            while ((c = inputStream.read(buf, 0, buf.length)) > 0) {
                outputStream.write(buf, 0, c);
            }

        } catch (IOException e) {
            Log.e(TAG, "", e);
            return false;
        } finally {
            org.yccheok.jstock.file.Utils.close(outputStream);
            org.yccheok.jstock.file.Utils.close(inputStream);
        }

        if (googleCloudFile == null) {
            // Create the metadata for the new file including title and MIME
            // type.
            MetadataChangeSet metadataChangeSet = new MetadataChangeSet.Builder()
                    .setTitle(title)
                    .setMimeType("application/zip").build();

            DriveFolder driveFolder = Drive.DriveApi.getAppFolder(googleApiClient);
            DriveFolder.DriveFileResult driveFileResult = driveFolder.createFile(googleApiClient, metadataChangeSet, driveContents).await();

            if (driveFileResult == null) {
                return false;
            }

            Status status = driveFileResult.getStatus();
            if (!status.isSuccess()) {
                h.handleStatus(status);
                return false;
            }
        } else {
            MetadataChangeSet metadataChangeSet = new MetadataChangeSet.Builder()
                    .setTitle(title).build();

            DriveResource.MetadataResult metadataResult = driveFile.updateMetadata(googleApiClient, metadataChangeSet).await();
            Status status = metadataResult.getStatus();
            if (!status.isSuccess()) {
                h.handleStatus(status);
                return false;
            }
        }

        Status status;
        try {
            status = driveContents.commit(googleApiClient, null).await();
        } catch (java.lang.IllegalStateException e) {
            // java.lang.IllegalStateException: DriveContents already closed.
            Log.e(TAG, "", e);
            return false;
        }

        if (!status.isSuccess()) {
            h.handleStatus(status);
            return false;
        }

        status = Drive.DriveApi.requestSync(googleApiClient).await();
        if (!status.isSuccess()) {
            // Sync request rate limit exceeded.
            //
            //h.handleStatus(status);
            //return false;
        }

        return true;
    } finally {
        if (googleCloudFile != null) {
            googleCloudFile.metadataBuffer.release();
        }
    }
}

Поиск существующего zip файла папки приложения

private static String getGoogleDriveTitle(long checksum, long date, int version) {
    return "jstock-" + org.yccheok.jstock.gui.Utils.getJStockUUID() + "-checksum=" + checksum + "-date=" + date + "-version=" + version + ".zip";
}

// https://stackoverflow.com/info/1360113/is-java-regex-thread-safe
private static final Pattern googleDocTitlePattern = Pattern.compile("jstock-" + org.yccheok.jstock.gui.Utils.getJStockUUID() + "-checksum=([0-9]+)-date=([0-9]+)-version=([0-9]+)\\.zip", Pattern.CASE_INSENSITIVE);

private static GoogleCloudFile searchFromGoogleDrive(GoogleApiClient googleApiClient, HandleStatusable h, PublishProgressable p) {
    DriveFolder driveFolder = Drive.DriveApi.getAppFolder(googleApiClient);

    // https://stackoverflow.com/info/34705929/filters-ownedbyme-doesnt-work-in-drive-api-for-android-but-works-correctly-i
    final String titleName = ("jstock-" + org.yccheok.jstock.gui.Utils.getJStockUUID() + "-checksum=");
    Query query = new Query.Builder()
            .addFilter(Filters.and(
                Filters.contains(SearchableField.TITLE, titleName),
                Filters.eq(SearchableField.TRASHED, false)
            ))
            .build();

    DriveApi.MetadataBufferResult metadataBufferResult = driveFolder.queryChildren(googleApiClient, query).await();

    if (metadataBufferResult == null) {
        return null;
    }

    Status status = metadataBufferResult.getStatus();

    if (!status.isSuccess()) {
        h.handleStatus(status);
        return null;
    }

    MetadataBuffer metadataBuffer = null;
    boolean needToReleaseMetadataBuffer = true;

    try {
        metadataBuffer = metadataBufferResult.getMetadataBuffer();
        if (metadataBuffer != null ) {
            long checksum = 0;
            long date = 0;
            int version = 0;
            Metadata metadata = null;

            for (Metadata md : metadataBuffer) {
                if (p.isCancelled()) {
                    return null;
                }

                if (md == null || !md.isDataValid()) {
                    continue;
                }

                final String title = md.getTitle();

                // Retrieve checksum, date and version information from filename.
                final Matcher matcher = googleDocTitlePattern.matcher(title);
                String _checksum = null;
                String _date = null;
                String _version = null;
                if (matcher.find()){
                    if (matcher.groupCount() == 3) {
                        _checksum = matcher.group(1);
                        _date = matcher.group(2);
                        _version = matcher.group(3);
                    }
                }
                if (_checksum == null || _date == null || _version == null) {
                    continue;
                }

                try {
                    checksum = Long.parseLong(_checksum);
                    date = Long.parseLong(_date);
                    version = Integer.parseInt(_version);
                } catch (NumberFormatException ex) {
                    Log.e(TAG, "", ex);
                    continue;
                }

                metadata = md;

                break;

            }   // for

            if (metadata != null) {
                // Caller will be responsible to release the resource. If release too early,
                // metadata will not readable.
                needToReleaseMetadataBuffer = false;
                return GoogleCloudFile.newInstance(metadataBuffer, metadata, checksum, date, version);
            }
        }   // if
    } finally {
        if (needToReleaseMetadataBuffer) {
            if (metadataBuffer != null) {
                metadataBuffer.release();
            }
        }
    }

    return null;
}

Проблема возникает при загрузке данных приложения. Представьте себе следующие операции

  • Загрузите ZIP-данные в папку приложения Google Диска в первый раз. Контрольная сумма 12345. Используемое имя файла ...checksum=12345...zip
  • Поиск zip-данных из папки приложений Google Диска. Возможность найти файл с именем файла ...checksum=12345...zip. Загрузите контент. Проверьте контрольную сумму содержимого 12345.
  • Перезаписать новые данные zip в существующий файл папки приложения Google Диска. Новая контрольная сумма данных zip составляет 67890. Существующий zip файл папки приложения переименован в ...checksum=67890...zip
  • Поиск zip-данных из папки приложений Google Диска. Возможность найти файл с именем файла ...checksum=67890...zip. Однако после загрузки содержимого контрольная сумма содержимого остается старой 12345!

Загрузить ZIP файл папки приложения

public static CloudFile loadFromGoogleDrive(GoogleApiClient googleApiClient, HandleStatusable h, PublishProgressable p) {
    final java.io.File directory = JStockApplication.instance().getExternalCacheDir();
    if (directory == null) {
        org.yccheok.jstock.gui.Utils.showLongToast(R.string.unable_to_access_external_storage);
        return null;
    }

    Status status = Drive.DriveApi.requestSync(googleApiClient).await();
    if (!status.isSuccess()) {
        // Sync request rate limit exceeded.
        //
        //h.handleStatus(status);
        //return null;
    }

    GoogleCloudFile googleCloudFile = searchFromGoogleDrive(googleApiClient, h, p);

    if (googleCloudFile == null) {
        return null;
    }

    try {
        DriveFile driveFile = googleCloudFile.metadata.getDriveId().asDriveFile();
        DriveApi.DriveContentsResult driveContentsResult = driveFile.open(googleApiClient, DriveFile.MODE_READ_ONLY, null).await();

        if (driveContentsResult == null) {
            return null;
        }

        status = driveContentsResult.getStatus();
        if (!status.isSuccess()) {
            h.handleStatus(status);
            return null;
        }

        final long checksum = googleCloudFile.checksum;
        final long date = googleCloudFile.date;
        final int version = googleCloudFile.version;

        p.publishProgress(JStockApplication.instance().getString(R.string.downloading));

        final DriveContents driveContents = driveContentsResult.getDriveContents();

        InputStream inputStream = null;
        java.io.File outputFile = null;
        OutputStream outputStream = null;

        try {
            inputStream = driveContents.getInputStream();
            outputFile = java.io.File.createTempFile(org.yccheok.jstock.gui.Utils.getJStockUUID(), ".zip", directory);
            outputFile.deleteOnExit();
            outputStream = new FileOutputStream(outputFile);

            int read = 0;
            byte[] bytes = new byte[1024];

            while ((read = inputStream.read(bytes)) != -1) {
                outputStream.write(bytes, 0, read);
            }
        } catch (IOException ex) {
            Log.e(TAG, "", ex);
        } finally {
            org.yccheok.jstock.file.Utils.close(outputStream);
            org.yccheok.jstock.file.Utils.close(inputStream);
            driveContents.discard(googleApiClient);
        }

        if (outputFile == null) {
            return null;
        }

        return CloudFile.newInstance(outputFile, checksum, date, version);
    } finally {
        googleCloudFile.metadataBuffer.release();
    }
}

Во-первых, я думал, что

Status status = Drive.DriveApi.requestSync(googleApiClient).await()

не работает хорошо. В большинстве случаев он не работает, с сообщением об ошибке Sync request rate limit exceeded. Фактически, жесткий предел, введенный в requestSync, делает этот API не особенно полезным - Android Google Play/Drive Api


Однако даже когда requestSync успех, loadFromGoogleDrive по-прежнему может получить только самое последнее имя файла, но устаревшее содержимое контрольной суммы.

Я на 100% уверен, что loadFromGoogleDrive возвращает мне содержимое кэшированных данных со следующими наблюдениями.

  • Я устанавливаю DownloadProgressListener в driveFile.open, bytesDownloaded равен 0 и bytesExpected равно -1.
  • Если я использую Google Rest Rest API со следующим код рабочего стола, я могу найти последнее имя файла с правильным содержимым контрольной суммы.
  • Если я удалю свое приложение для Android и снова заново установлю, loadFromGoogleDrive сможет получить последнее имя файла с правильным содержимым контрольной суммы.

Есть ли какой-либо надежный способ избежать загрузки данных кэшированных приложений с Google Диска?


Мне удалось создать демоверсию. Вот шаги для воспроизведения этой проблемы.

Шаг 1: Загрузите исходный код

https://github.com/yccheok/google-drive-bug

Шаг 2: Настройка в консоли API

введите описание изображения здесь

Шаг 3: Нажмите кнопку SAVE "123.TXT" С СОДЕРЖАНИЕМ "123"

введите описание изображения здесь

В папке приложения будет создан файл с именем файла "123.TXT", содержимое "123" .

Шаг 4: Нажмите кнопку SAVE "456.TXT" С СОДЕРЖАНИЕМ "456"

введите описание изображения здесь

Предыдущий файл будет переименован в "456.TXT", содержимое будет обновлено до "456"

Шаг 5: Нажмите кнопку LOAD LAST SAVED FILE

введите описание изображения здесь

Файл с именем файла "456.TXT" был найден, но прочитан предыдущий кешированный контент "123" . Я ожидал контента "456".

Заметим, что если мы

  • Удалить демонстрационное приложение.
  • Повторно установите демонстрационное приложение.
  • Нажмите кнопку LOAD LAST SAVED FILE, файл с именем файла "456.TXT" и содержимым "456" найдено.

Я официально представил отчет о проблемах - https://code.google.com/a/google.com/p/apps-api-issues/issues/detail?id=4727


Другая информация

Вот как это выглядит под моим устройством - http://youtu.be/kuIHoi4A1c0

Я понимаю, что не все пользователи будут сталкиваться с этой проблемой. Например, я тестировал другой Nexus 6, Google Play Services 9.4.52 (440-127739847). Проблема не появляется.

Я собрал APK для тестирования - https://github.com/yccheok/google-drive-bug/releases/download/1.0/demo.apk

4b9b3361

Ответ 1

  • Поиск на Google Диске медленный. Почему бы не использовать свойства базовой папки для хранения идентификатора zip файла? https://developers.google.com/drive/v2/web/properties
  • Имена файлов на Google Диске не уникальны, вы можете загружать несколько файлов с одинаковыми именами. Идентификатор файла, возвращенный Google, однако, уникален.