mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 12:23:58 +01:00
Handle errors in chunk uploads (#3899)
This commit is contained in:
parent
1856196851
commit
74720aac19
8 changed files with 99 additions and 14 deletions
|
|
@ -3,6 +3,9 @@ package fr.free.nrw.commons;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import okhttp3.Cache;
|
import okhttp3.Cache;
|
||||||
import okhttp3.Interceptor;
|
import okhttp3.Interceptor;
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
|
|
@ -62,6 +65,8 @@ public final class OkHttpConnectionFactory {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class UnsuccessfulResponseInterceptor implements Interceptor {
|
public static class UnsuccessfulResponseInterceptor implements Interceptor {
|
||||||
|
private static final List<String> DO_NOT_INTERCEPT = Collections.singletonList(
|
||||||
|
"api.php?format=json&formatversion=2&errorformat=plaintext&action=upload&ignorewarnings=1");
|
||||||
|
|
||||||
private static final String ERRORS_PREFIX = "{\"error";
|
private static final String ERRORS_PREFIX = "{\"error";
|
||||||
|
|
||||||
|
|
@ -69,6 +74,11 @@ public final class OkHttpConnectionFactory {
|
||||||
@NonNull
|
@NonNull
|
||||||
public Response intercept(@NonNull final Chain chain) throws IOException {
|
public Response intercept(@NonNull final Chain chain) throws IOException {
|
||||||
final Response rsp = chain.proceed(chain.request());
|
final Response rsp = chain.proceed(chain.request());
|
||||||
|
|
||||||
|
// Do not intercept certain requests and let the caller handle the errors
|
||||||
|
if(isExcludedUrl(chain.request())) {
|
||||||
|
return rsp;
|
||||||
|
}
|
||||||
if (rsp.isSuccessful()) {
|
if (rsp.isSuccessful()) {
|
||||||
try (final ResponseBody responseBody = rsp.peekBody(ERRORS_PREFIX.length())) {
|
try (final ResponseBody responseBody = rsp.peekBody(ERRORS_PREFIX.length())) {
|
||||||
if (ERRORS_PREFIX.equals(responseBody.string())) {
|
if (ERRORS_PREFIX.equals(responseBody.string())) {
|
||||||
|
|
@ -83,6 +93,16 @@ public final class OkHttpConnectionFactory {
|
||||||
}
|
}
|
||||||
throw new HttpStatusException(rsp);
|
throw new HttpStatusException(rsp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isExcludedUrl(final Request request) {
|
||||||
|
final String requestUrl = request.url().toString();
|
||||||
|
for(final String url: DO_NOT_INTERCEPT) {
|
||||||
|
if(requestUrl.contains(url)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private OkHttpConnectionFactory() {
|
private OkHttpConnectionFactory() {
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,23 @@ package fr.free.nrw.commons.contributions
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import fr.free.nrw.commons.upload.UploadResult
|
import fr.free.nrw.commons.upload.UploadResult
|
||||||
import kotlinx.android.parcel.Parcelize
|
|
||||||
|
|
||||||
data class ChunkInfo(
|
data class ChunkInfo(
|
||||||
val uploadResult: UploadResult,
|
val uploadResult: UploadResult,
|
||||||
val lastChunkIndex: Int
|
val lastChunkIndex: Int,
|
||||||
|
var isLastChunkUploaded: Boolean
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
constructor(parcel: Parcel) : this(
|
constructor(parcel: Parcel) : this(
|
||||||
parcel.readParcelable(UploadResult::class.java.classLoader),
|
parcel.readParcelable(UploadResult::class.java.classLoader),
|
||||||
parcel.readInt()
|
parcel.readInt(),
|
||||||
|
parcel.readByte() != 0.toByte()
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
parcel.writeParcelable(uploadResult, flags)
|
parcel.writeParcelable(uploadResult, flags)
|
||||||
parcel.writeInt(lastChunkIndex)
|
parcel.writeInt(lastChunkIndex)
|
||||||
|
parcel.writeByte(if (isLastChunkUploaded) 1 else 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
override fun describeContents(): Int {
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,13 @@ import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import com.google.gson.Gson;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import fr.free.nrw.commons.CommonsApplication;
|
||||||
import fr.free.nrw.commons.contributions.ChunkInfo;
|
import fr.free.nrw.commons.contributions.ChunkInfo;
|
||||||
import fr.free.nrw.commons.contributions.Contribution;
|
import fr.free.nrw.commons.contributions.Contribution;
|
||||||
import fr.free.nrw.commons.upload.UploadService.NotificationUpdateProgressListener;
|
import fr.free.nrw.commons.upload.UploadService.NotificationUpdateProgressListener;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
import io.reactivex.functions.Consumer;
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
@ -24,6 +24,7 @@ import okhttp3.MediaType;
|
||||||
import okhttp3.MultipartBody;
|
import okhttp3.MultipartBody;
|
||||||
import okhttp3.RequestBody;
|
import okhttp3.RequestBody;
|
||||||
import org.wikipedia.csrf.CsrfTokenClient;
|
import org.wikipedia.csrf.CsrfTokenClient;
|
||||||
|
import org.wikipedia.dataclient.mwapi.MwException;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
|
|
@ -39,6 +40,7 @@ public class UploadClient {
|
||||||
private final CsrfTokenClient csrfTokenClient;
|
private final CsrfTokenClient csrfTokenClient;
|
||||||
private final PageContentsCreator pageContentsCreator;
|
private final PageContentsCreator pageContentsCreator;
|
||||||
private final FileUtilsWrapper fileUtilsWrapper;
|
private final FileUtilsWrapper fileUtilsWrapper;
|
||||||
|
private final Gson gson;
|
||||||
private boolean pauseUploads = false;
|
private boolean pauseUploads = false;
|
||||||
|
|
||||||
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
|
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||||
|
|
@ -47,11 +49,12 @@ public class UploadClient {
|
||||||
public UploadClient(final UploadInterface uploadInterface,
|
public UploadClient(final UploadInterface uploadInterface,
|
||||||
@Named(NAMED_COMMONS_CSRF) final CsrfTokenClient csrfTokenClient,
|
@Named(NAMED_COMMONS_CSRF) final CsrfTokenClient csrfTokenClient,
|
||||||
final PageContentsCreator pageContentsCreator,
|
final PageContentsCreator pageContentsCreator,
|
||||||
final FileUtilsWrapper fileUtilsWrapper) {
|
final FileUtilsWrapper fileUtilsWrapper, final Gson gson) {
|
||||||
this.uploadInterface = uploadInterface;
|
this.uploadInterface = uploadInterface;
|
||||||
this.csrfTokenClient = csrfTokenClient;
|
this.csrfTokenClient = csrfTokenClient;
|
||||||
this.pageContentsCreator = pageContentsCreator;
|
this.pageContentsCreator = pageContentsCreator;
|
||||||
this.fileUtilsWrapper = fileUtilsWrapper;
|
this.fileUtilsWrapper = fileUtilsWrapper;
|
||||||
|
this.gson = gson;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -61,6 +64,10 @@ public class UploadClient {
|
||||||
Observable<StashUploadResult> uploadFileToStash(
|
Observable<StashUploadResult> uploadFileToStash(
|
||||||
final Context context, final String filename, final Contribution contribution,
|
final Context context, final String filename, final Contribution contribution,
|
||||||
final NotificationUpdateProgressListener notificationUpdater) throws IOException {
|
final NotificationUpdateProgressListener notificationUpdater) throws IOException {
|
||||||
|
if (contribution.getChunkInfo() != null && contribution.getChunkInfo().isLastChunkUploaded()) {
|
||||||
|
return Observable.just(new StashUploadResult(StashUploadState.SUCCESS,
|
||||||
|
contribution.getChunkInfo().getUploadResult().getFilekey()));
|
||||||
|
}
|
||||||
pauseUploads = false;
|
pauseUploads = false;
|
||||||
File file = new File(contribution.getLocalUri().getPath());
|
File file = new File(contribution.getLocalUri().getPath());
|
||||||
final Observable<File> fileChunks = fileUtilsWrapper.getFileChunks(context, file, CHUNK_SIZE);
|
final Observable<File> fileChunks = fileUtilsWrapper.getFileChunks(context, file, CHUNK_SIZE);
|
||||||
|
|
@ -69,6 +76,7 @@ public class UploadClient {
|
||||||
|
|
||||||
final AtomicInteger index = new AtomicInteger();
|
final AtomicInteger index = new AtomicInteger();
|
||||||
final AtomicReference<ChunkInfo> chunkInfo = new AtomicReference<>();
|
final AtomicReference<ChunkInfo> chunkInfo = new AtomicReference<>();
|
||||||
|
Timber.d("Chunk info");
|
||||||
if (contribution.getChunkInfo() != null && isStashValid(contribution)) {
|
if (contribution.getChunkInfo() != null && isStashValid(contribution)) {
|
||||||
chunkInfo.set(contribution.getChunkInfo());
|
chunkInfo.set(contribution.getChunkInfo());
|
||||||
}
|
}
|
||||||
|
|
@ -96,12 +104,15 @@ public class UploadClient {
|
||||||
offset,
|
offset,
|
||||||
filekey,
|
filekey,
|
||||||
countingRequestBody).subscribe(uploadResult -> {
|
countingRequestBody).subscribe(uploadResult -> {
|
||||||
chunkInfo.set(new ChunkInfo(uploadResult, index.incrementAndGet()));
|
chunkInfo.set(new ChunkInfo(uploadResult, index.incrementAndGet(), false));
|
||||||
notificationUpdater.onChunkUploaded(contribution, chunkInfo.get());
|
notificationUpdater.onChunkUploaded(contribution, chunkInfo.get());
|
||||||
}, throwable -> {
|
}, throwable -> {
|
||||||
Timber.e(throwable, "Error occurred in uploading chunk");
|
Timber.e(throwable, "Error occurred in uploading chunk");
|
||||||
}));
|
}));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
chunkInfo.get().setLastChunkUploaded(true);
|
||||||
|
notificationUpdater.onChunkUploaded(contribution, chunkInfo.get());
|
||||||
if (pauseUploads) {
|
if (pauseUploads) {
|
||||||
return Observable.just(new StashUploadResult(StashUploadState.PAUSED, null));
|
return Observable.just(new StashUploadResult(StashUploadState.PAUSED, null));
|
||||||
} else if (chunkInfo.get() != null) {
|
} else if (chunkInfo.get() != null) {
|
||||||
|
|
@ -183,9 +194,16 @@ public class UploadClient {
|
||||||
pageContentsCreator.createFrom(contribution),
|
pageContentsCreator.createFrom(contribution),
|
||||||
CommonsApplication.DEFAULT_EDIT_SUMMARY,
|
CommonsApplication.DEFAULT_EDIT_SUMMARY,
|
||||||
uniqueFileName,
|
uniqueFileName,
|
||||||
fileKey).map(UploadResponse::getUpload);
|
fileKey).map(uploadResponse -> {
|
||||||
|
UploadResponse uploadResult = gson.fromJson(uploadResponse, UploadResponse.class);
|
||||||
|
if (uploadResult.getUpload() == null) {
|
||||||
|
final MwException exception = gson.fromJson(uploadResponse, MwException.class);
|
||||||
|
throw new RuntimeException(exception.getErrorCode());
|
||||||
|
}
|
||||||
|
return uploadResult.getUpload();
|
||||||
|
});
|
||||||
} catch (final Throwable throwable) {
|
} catch (final Throwable throwable) {
|
||||||
throwable.printStackTrace();
|
Timber.e(throwable, "Exception occurred in uploading file from stash");
|
||||||
return Observable.error(throwable);
|
return Observable.error(throwable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
import okhttp3.MultipartBody;
|
import okhttp3.MultipartBody;
|
||||||
import okhttp3.RequestBody;
|
import okhttp3.RequestBody;
|
||||||
|
|
@ -29,7 +30,7 @@ public interface UploadInterface {
|
||||||
@POST(MW_API_PREFIX + "action=upload&ignorewarnings=1")
|
@POST(MW_API_PREFIX + "action=upload&ignorewarnings=1")
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@NonNull
|
@NonNull
|
||||||
Observable<UploadResponse> uploadFileFromStash(@NonNull @Field("token") String token,
|
Observable<JsonObject> uploadFileFromStash(@NonNull @Field("token") String token,
|
||||||
@NonNull @Field("text") String text,
|
@NonNull @Field("text") String text,
|
||||||
@NonNull @Field("comment") String comment,
|
@NonNull @Field("comment") String comment,
|
||||||
@NonNull @Field("filename") String filename,
|
@NonNull @Field("filename") String filename,
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,14 @@ import fr.free.nrw.commons.wikidata.WikidataEditService;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
import io.reactivex.Scheduler;
|
import io.reactivex.Scheduler;
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
|
import io.reactivex.functions.Consumer;
|
||||||
import io.reactivex.processors.PublishProcessor;
|
import io.reactivex.processors.PublishProcessor;
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
@ -42,6 +45,9 @@ public class UploadService extends CommonsDaggerService {
|
||||||
|
|
||||||
private static final String EXTRA_PREFIX = "fr.free.nrw.commons.upload";
|
private static final String EXTRA_PREFIX = "fr.free.nrw.commons.upload";
|
||||||
|
|
||||||
|
private static final List<String> STASH_ERROR_CODES = Arrays
|
||||||
|
.asList("uploadstash-file-not-found", "stashfailed", "verification-error", "chunk-too-small");
|
||||||
|
|
||||||
public static final String ACTION_START_SERVICE = EXTRA_PREFIX + ".upload";
|
public static final String ACTION_START_SERVICE = EXTRA_PREFIX + ".upload";
|
||||||
public static final String EXTRA_FILES = EXTRA_PREFIX + ".files";
|
public static final String EXTRA_FILES = EXTRA_PREFIX + ".files";
|
||||||
@Inject
|
@Inject
|
||||||
|
|
@ -290,7 +296,15 @@ public class UploadService extends CommonsDaggerService {
|
||||||
getApplicationContext(),
|
getApplicationContext(),
|
||||||
contribution,
|
contribution,
|
||||||
uniqueFilename,
|
uniqueFilename,
|
||||||
uploadStash.getFileKey());
|
uploadStash.getFileKey()).doOnError(new Consumer<Throwable>() {
|
||||||
|
@Override
|
||||||
|
public void accept(Throwable throwable) throws Exception {
|
||||||
|
Timber.e(throwable, "Error occurred in uploading file from stash");
|
||||||
|
if (STASH_ERROR_CODES.contains(throwable.getMessage())) {
|
||||||
|
clearChunks(contribution);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
} else if (uploadStash.getState() == StashUploadState.PAUSED) {
|
} else if (uploadStash.getState() == StashUploadState.PAUSED) {
|
||||||
Timber.d("Contribution upload paused");
|
Timber.d("Contribution upload paused");
|
||||||
showPausedNotification(contribution);
|
showPausedNotification(contribution);
|
||||||
|
|
@ -310,6 +324,13 @@ public class UploadService extends CommonsDaggerService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void clearChunks(Contribution contribution) {
|
||||||
|
contribution.setChunkInfo(null);
|
||||||
|
compositeDisposable.add(contributionDao.update(contribution)
|
||||||
|
.subscribeOn(ioThreadScheduler)
|
||||||
|
.subscribe());
|
||||||
|
}
|
||||||
|
|
||||||
private void onUpload(Contribution contribution, String notificationTag,
|
private void onUpload(Contribution contribution, String notificationTag,
|
||||||
UploadResult uploadResult) {
|
UploadResult uploadResult) {
|
||||||
Timber.d("Stash upload response 2 is %s", uploadResult.toString());
|
Timber.d("Stash upload response 2 is %s", uploadResult.toString());
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,32 @@ package org.wikipedia.dataclient.mwapi;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class MwException extends RuntimeException {
|
public class MwException extends RuntimeException {
|
||||||
@SuppressWarnings("unused") @NonNull private final MwServiceError error;
|
@SuppressWarnings("unused") @Nullable private final MwServiceError error;
|
||||||
|
|
||||||
public MwException(@NonNull MwServiceError error) {
|
@SuppressWarnings("unused") @Nullable private final List<MwServiceError> errors;
|
||||||
|
|
||||||
|
public MwException(@Nullable MwServiceError error,
|
||||||
|
@Nullable final List<MwServiceError> errors) {
|
||||||
this.error = error;
|
this.error = error;
|
||||||
|
this.errors = errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull public MwServiceError getError() {
|
@NonNull
|
||||||
|
public List<MwServiceError> getErrors() {
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getErrorCode() {
|
||||||
|
if(error!=null) {
|
||||||
|
return error.getCode();
|
||||||
|
}
|
||||||
|
return errors != null ? errors.get(0).getCode() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable public MwServiceError getError() {
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ public abstract class MwResponse extends BaseModel implements PostProcessingType
|
||||||
@Override
|
@Override
|
||||||
public void postProcess() {
|
public void postProcess() {
|
||||||
if (errors != null && !errors.isEmpty()) {
|
if (errors != null && !errors.isEmpty()) {
|
||||||
throw new MwException(errors.get(0));
|
throw new MwException(errors.get(0), errors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import java.util.List;
|
||||||
* Gson POJO for a MediaWiki API error.
|
* Gson POJO for a MediaWiki API error.
|
||||||
*/
|
*/
|
||||||
public class MwServiceError extends BaseModel implements ServiceError {
|
public class MwServiceError extends BaseModel implements ServiceError {
|
||||||
|
|
||||||
@SuppressWarnings("unused") @Nullable private String code;
|
@SuppressWarnings("unused") @Nullable private String code;
|
||||||
@SuppressWarnings("unused") @Nullable private String text;
|
@SuppressWarnings("unused") @Nullable private String text;
|
||||||
@SuppressWarnings("unused") @Nullable private Data data;
|
@SuppressWarnings("unused") @Nullable private Data data;
|
||||||
|
|
@ -55,6 +56,11 @@ public class MwServiceError extends BaseModel implements ServiceError {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public String getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
private static final class Data {
|
private static final class Data {
|
||||||
@SuppressWarnings("unused") @Nullable private List<Message> messages;
|
@SuppressWarnings("unused") @Nullable private List<Message> messages;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue