Skip to content

Commit 301bbc8

Browse files
authored
Support media single fetch, upload, and delete using WordPress-rs (#22036)
* Adding fetch media * Handling media error * Adding delete media * Fix refactor * Uploading media * Adding debug logs * minor refactor * Extracting client build code * Fixing completion issue * Adding the cancellation function * Some cleaning * detekt * Reverting cancellation * Removing unused function * Copilot suggestions * Updating wordpress-rs and minor fox
1 parent e5bf5b4 commit 301bbc8

File tree

3 files changed

+223
-20
lines changed

3 files changed

+223
-20
lines changed

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ wiremock = '2.26.3'
103103
wordpress-aztec = 'v2.1.4'
104104
wordpress-lint = '2.2.0'
105105
wordpress-persistent-edittext = '1.0.2'
106-
wordpress-rs = 'trunk-1ae11b4e897192f5064912d201e92539eb0b3416'
106+
wordpress-rs = 'trunk-0d7174336f381588dd577e60384225fa7b9f80c4'
107107
wordpress-utils = '3.14.0'
108108
automattic-ucrop = '2.2.11'
109109
zendesk = '5.5.0'

libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/media/MediaRSApiRestClient.kt

Lines changed: 213 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,24 @@ import kotlinx.coroutines.CoroutineScope
44
import kotlinx.coroutines.launch
55
import org.wordpress.android.fluxc.Dispatcher
66
import org.wordpress.android.fluxc.generated.MediaActionBuilder
7+
import org.wordpress.android.fluxc.generated.UploadActionBuilder
78
import org.wordpress.android.fluxc.model.MediaModel
9+
import org.wordpress.android.fluxc.model.MediaModel.MediaUploadState
810
import org.wordpress.android.fluxc.model.SiteModel
911
import org.wordpress.android.fluxc.module.FLUXC_SCOPE
1012
import org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords.WpAppNotifierHandler
1113
import org.wordpress.android.fluxc.store.MediaStore.FetchMediaListResponsePayload
14+
import org.wordpress.android.fluxc.store.MediaStore.MediaError
15+
import org.wordpress.android.fluxc.store.MediaStore.MediaErrorType
16+
import org.wordpress.android.fluxc.store.MediaStore.MediaPayload
17+
import org.wordpress.android.fluxc.store.MediaStore.ProgressPayload
1218
import org.wordpress.android.fluxc.utils.AppLogWrapper
19+
import org.wordpress.android.fluxc.utils.MediaUtils
1320
import org.wordpress.android.fluxc.utils.MimeType
1421
import org.wordpress.android.util.AppLog
1522
import rs.wordpress.api.kotlin.WpApiClient
1623
import rs.wordpress.api.kotlin.WpRequestResult
24+
import uniffi.wp_api.MediaCreateParams
1725
import uniffi.wp_api.MediaDetailsPayload
1826
import uniffi.wp_api.MediaListParams
1927
import uniffi.wp_api.MediaWithEditContext
@@ -36,19 +44,7 @@ class MediaRSApiRestClient @Inject constructor(
3644
) {
3745
fun fetchMediaList(site: SiteModel, number: Int, offset: Int, mimeType: MimeType.Type?) {
3846
scope.launch {
39-
val authProvider = WpAuthenticationProvider.staticWithUsernameAndPassword(
40-
username = site.apiRestUsernamePlain, password = site.apiRestPasswordPlain
41-
)
42-
val apiRootUrl = URL(site.buildUrl())
43-
val client = WpApiClient(
44-
wpOrgSiteApiRootUrl = apiRootUrl,
45-
authProvider = authProvider,
46-
appNotifier = object : WpAppNotifier {
47-
override suspend fun requestedWithInvalidAuthentication() {
48-
wpAppNotifierHandler.notifyRequestedWithInvalidAuthentication(site)
49-
}
50-
}
51-
)
47+
val client = getWpApiClient(site)
5248
val mediaResponse = client.request { requestBuilder ->
5349
requestBuilder.media().listWithEditContext(
5450
MediaListParams(
@@ -62,12 +58,12 @@ class MediaRSApiRestClient @Inject constructor(
6258

6359
val mediaModelList = when (mediaResponse) {
6460
is WpRequestResult.Success -> {
65-
appLogWrapper.d(AppLog.T.MAIN, "Fetched media list: ${mediaResponse.response.data.size}")
61+
appLogWrapper.d(AppLog.T.MEDIA, "Fetched media list: ${mediaResponse.response.data.size}")
6662
mediaResponse.response.data.toMediaModelList(site.id)
6763
}
6864

6965
else -> {
70-
appLogWrapper.e(AppLog.T.MAIN, "Fetch media list failed: $mediaResponse")
66+
appLogWrapper.e(AppLog.T.MEDIA, "Fetch media list failed: $mediaResponse")
7167
emptyList()
7268
}
7369
}
@@ -92,6 +88,207 @@ class MediaRSApiRestClient @Inject constructor(
9288
dispatcher.dispatch(MediaActionBuilder.newFetchedMediaListAction(payload))
9389
}
9490

91+
fun fetchMedia(site: SiteModel, media: MediaModel?) {
92+
if (media == null) {
93+
val error = MediaError(MediaErrorType.NULL_MEDIA_ARG)
94+
error.logMessage = "Requested media is null"
95+
notifyMediaFetched(site, null, error)
96+
return
97+
}
98+
99+
scope.launch {
100+
val client = getWpApiClient(site)
101+
102+
val mediaResponse = client.request { requestBuilder ->
103+
requestBuilder.media().retrieveWithEditContext(media.mediaId)
104+
}
105+
106+
107+
when (mediaResponse) {
108+
is WpRequestResult.Success -> {
109+
appLogWrapper.d(AppLog.T.MEDIA, "Fetched media with ID: " + media.mediaId)
110+
111+
val responseMedia: MediaModel = mediaResponse.response.data.toMediaModel(site.id).apply {
112+
localSiteId = site.id
113+
}
114+
notifyMediaFetched(site, responseMedia, null)
115+
}
116+
117+
else -> {
118+
val mediaError = parseMediaError(mediaResponse)
119+
appLogWrapper.e(AppLog.T.MEDIA, "Fetch media failed: ${mediaError.message}")
120+
notifyMediaFetched(site, media, mediaError)
121+
}
122+
}
123+
}
124+
}
125+
126+
@Suppress("UseCheckOrError") // Allow to throw IllegalStateException
127+
private fun parseMediaError(mediaResponse: WpRequestResult<*>): MediaError {
128+
return when (mediaResponse) {
129+
is WpRequestResult.Success -> {
130+
throw IllegalStateException("Success media response should not be parsed as an error")
131+
}
132+
is WpRequestResult.MediaFileNotFound<*> -> {
133+
appLogWrapper.e(AppLog.T.MEDIA, "Media file not found: $mediaResponse")
134+
MediaError(MediaErrorType.NOT_FOUND).apply {
135+
message = "Media file not found"
136+
}
137+
}
138+
139+
is WpRequestResult.ResponseParsingError<*> -> {
140+
appLogWrapper.e(AppLog.T.MEDIA, "Response parsing error: $mediaResponse")
141+
MediaError(MediaErrorType.PARSE_ERROR).apply {
142+
message = "Failed to parse response"
143+
}
144+
}
145+
146+
is WpRequestResult.SiteUrlParsingError<*> -> {
147+
appLogWrapper.e(AppLog.T.MEDIA, "Site URL parsing error: $mediaResponse")
148+
MediaError(MediaErrorType.MALFORMED_MEDIA_ARG).apply {
149+
message = "Invalid site URL"
150+
}
151+
}
152+
153+
is WpRequestResult.InvalidHttpStatusCode<*>,
154+
is WpRequestResult.WpError<*>,
155+
is WpRequestResult.RequestExecutionFailed<*>,
156+
is WpRequestResult.UnknownError<*> -> {
157+
appLogWrapper.e(AppLog.T.MEDIA, "Unknown error: $mediaResponse")
158+
MediaError(MediaErrorType.GENERIC_ERROR).apply {
159+
message = "Unknown error occurred"
160+
}
161+
}
162+
}
163+
}
164+
165+
private fun notifyMediaFetched(
166+
site: SiteModel,
167+
media: MediaModel?,
168+
error: MediaError?
169+
) {
170+
val payload = MediaPayload(site, media, error)
171+
dispatcher.dispatch(MediaActionBuilder.newFetchedMediaAction(payload))
172+
}
173+
174+
fun deleteMedia(site: SiteModel, media: MediaModel?) {
175+
if (media == null) {
176+
val error = MediaError(MediaErrorType.NULL_MEDIA_ARG)
177+
error.logMessage = "Media to delete is null"
178+
notifyMediaDeleted(site, null, error)
179+
return
180+
}
181+
182+
scope.launch {
183+
val client = getWpApiClient(site)
184+
185+
val mediaResponse = client.request { requestBuilder ->
186+
requestBuilder.media().delete(media.mediaId)
187+
}
188+
189+
when (mediaResponse) {
190+
is WpRequestResult.Success -> {
191+
appLogWrapper.d(AppLog.T.MEDIA, "Deleted media with ID: " + media.mediaId)
192+
193+
val responseMedia: MediaModel = mediaResponse.response.data.previous.toMediaModel(site.id).apply {
194+
localSiteId = site.id
195+
}
196+
notifyMediaDeleted(site, responseMedia, null)
197+
}
198+
199+
else -> {
200+
val mediaError = parseMediaError(mediaResponse)
201+
appLogWrapper.e(AppLog.T.MEDIA, "Delete media failed: ${mediaError.message}")
202+
notifyMediaDeleted(site, media, mediaError)
203+
}
204+
}
205+
}
206+
}
207+
208+
private fun notifyMediaDeleted(
209+
site: SiteModel,
210+
media: MediaModel?,
211+
error: MediaError?
212+
) {
213+
val payload = MediaPayload(site, media, error)
214+
dispatcher.dispatch(MediaActionBuilder.newDeletedMediaAction(payload))
215+
}
216+
217+
fun uploadMedia(site: SiteModel, media: MediaModel?) {
218+
if (media == null || media.id == 0) {
219+
// we can't have a MediaModel without an ID - otherwise we can't keep track of them.
220+
val error = MediaError(MediaErrorType.INVALID_ID)
221+
if (media == null) {
222+
error.logMessage = "Media object is null on upload"
223+
} else {
224+
error.logMessage = "Media ID is 0 on upload"
225+
}
226+
notifyMediaUploaded(media, error)
227+
return
228+
}
229+
230+
if (media.filePath == null || !MediaUtils.canReadFile(media.filePath)) {
231+
val error = MediaError(MediaErrorType.FS_READ_PERMISSION_DENIED)
232+
error.logMessage = "Can't read file on upload"
233+
notifyMediaUploaded(media, error)
234+
return
235+
}
236+
237+
scope.launch {
238+
val client = getWpApiClient(site)
239+
240+
val mediaResponse = client.request { requestBuilder ->
241+
requestBuilder.media().create(
242+
params = MediaCreateParams(title = media.title),
243+
filePath = media.filePath!!, // We have already checked the nullability but it's mutable
244+
fileContentType = media.mimeType.orEmpty(),
245+
requestId = null
246+
)
247+
}
248+
249+
when (mediaResponse) {
250+
is WpRequestResult.Success -> {
251+
appLogWrapper.d(AppLog.T.MEDIA, "Uploaded media with ID: " + media.id)
252+
253+
val responseMedia: MediaModel = mediaResponse.response.data.toMediaModel(site.id).apply {
254+
id = media.id // be sure we are using the same local id when getting the remote response
255+
localSiteId = site.id
256+
}
257+
notifyMediaUploaded(responseMedia, null)
258+
}
259+
260+
else -> {
261+
val mediaError = parseMediaError(mediaResponse)
262+
appLogWrapper.e(AppLog.T.MEDIA, "Upload media failed: ${mediaError.message}")
263+
notifyMediaUploaded(media, mediaError)
264+
}
265+
}
266+
}
267+
}
268+
269+
private fun notifyMediaUploaded(media: MediaModel?, error: MediaError?) {
270+
media?.setUploadState(if (error == null) MediaUploadState.UPLOADED else MediaUploadState.FAILED)
271+
val payload = ProgressPayload(media, 1f, error == null, error)
272+
dispatcher.dispatch(UploadActionBuilder.newUploadedMediaAction(payload))
273+
}
274+
275+
private fun getWpApiClient(site: SiteModel): WpApiClient {
276+
val authProvider = WpAuthenticationProvider.staticWithUsernameAndPassword(
277+
username = site.apiRestUsernamePlain, password = site.apiRestPasswordPlain
278+
)
279+
val apiRootUrl = URL(site.buildUrl())
280+
val client = WpApiClient(
281+
wpOrgSiteApiRootUrl = apiRootUrl,
282+
authProvider = authProvider,
283+
appNotifier = object : WpAppNotifier {
284+
override suspend fun requestedWithInvalidAuthentication() {
285+
wpAppNotifierHandler.notifyRequestedWithInvalidAuthentication(site)
286+
}
287+
}
288+
)
289+
return client
290+
}
291+
95292
private fun List<MediaWithEditContext>.toMediaModelList(
96293
siteId: Int
97294
): List<MediaModel> = map { it.toMediaModel(siteId) }
@@ -110,7 +307,7 @@ class MediaRSApiRestClient @Inject constructor(
110307
fileExtension = this@toMediaModel.mediaType.toString()
111308
uploadDate = this@toMediaModel.date
112309
authorId = this@toMediaModel.author
113-
uploadState = org.wordpress.android.fluxc.model.MediaModel.MediaUploadState.UPLOADED.toString()
310+
uploadState = MediaUploadState.UPLOADED.toString()
114311

115312
// Parse the media details
116313
when (val parsedType = this@toMediaModel.mediaDetails.parseAsMimeType(this@toMediaModel.mimeType)) {

libs/fluxc/src/main/java/org/wordpress/android/fluxc/store/MediaStore.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -938,7 +938,9 @@ private void performUploadMedia(@NonNull UploadMediaPayload payload) {
938938
MediaUtils.stripLocation(payload.media.getFilePath());
939939
}
940940

941-
if (payload.site.isUsingWpComRestApi()) {
941+
if (payload.site.isUsingSelfHostedRestApi()) {
942+
mMediaRSApiRestClient.uploadMedia(payload.site, payload.media);
943+
} else if (payload.site.isUsingWpComRestApi()) {
942944
mMediaRestClient.uploadMedia(payload.site, payload.media);
943945
} else if (payload.site.isJetpackCPConnected()) {
944946
mWPComV2MediaRestClient.uploadMedia(payload.site, payload.media);
@@ -983,7 +985,9 @@ private void performFetchMedia(@NonNull MediaPayload payload) {
983985
return;
984986
}
985987

986-
if (payload.site.isUsingWpComRestApi()) {
988+
if (payload.site.isUsingSelfHostedRestApi()) {
989+
mMediaRSApiRestClient.fetchMedia(payload.site, payload.media);
990+
} else if (payload.site.isUsingWpComRestApi()) {
987991
mMediaRestClient.fetchMedia(payload.site, payload.media);
988992
} else if (payload.site.isJetpackCPConnected()) {
989993
mWPComV2MediaRestClient.fetchMedia(payload.site, payload.media);
@@ -998,7 +1002,9 @@ private void performDeleteMedia(@NonNull MediaPayload payload) {
9981002
return;
9991003
}
10001004

1001-
if (payload.site.isUsingWpComRestApi()) {
1005+
if (payload.site.isUsingSelfHostedRestApi()) {
1006+
mMediaRSApiRestClient.deleteMedia(payload.site, payload.media);
1007+
} else if (payload.site.isUsingWpComRestApi()) {
10021008
mMediaRestClient.deleteMedia(payload.site, payload.media);
10031009
} else {
10041010
mMediaXmlrpcClient.deleteMedia(payload.site, payload.media);

0 commit comments

Comments
 (0)