1
1
// @ts -check
2
2
import { pinningServiceTemplates } from '../constants/pinning'
3
3
import memoize from 'p-memoize'
4
+ import CID from 'cids'
5
+
6
+ // This bundle leverages createCacheBundle and persistActions for
7
+ // the persistence layer that keeps pins in IndexDB store
8
+ // to ensure they are around across restarts/reloads/refactors/releases.
9
+
10
+ const CID_PIN_CHECK_BATCH_SIZE = 10 // Pinata returns error when >10
11
+
12
+ // id = `${serviceName}:${cid}`
13
+ const cacheId2Cid = ( id ) => id . split ( ':' ) . at ( - 1 )
14
+ const cacheId2ServiceName = ( id ) => id . split ( ':' ) . at ( 0 )
4
15
5
16
const parseService = async ( service , remoteServiceTemplates , ipfs ) => {
6
17
const template = remoteServiceTemplates . find ( t => service . endpoint . toString ( ) === t . apiEndpoint . toString ( ) )
7
18
const icon = template ?. icon
8
19
const visitServiceUrl = template ?. visitServiceUrl
9
- const autoUpload = await mfsPolicyEnableFlag ( service . service , ipfs )
10
- const parsedService = { ...service , name : service . service , icon, visitServiceUrl, autoUpload }
20
+ const parsedService = { ...service , name : service . service , icon, visitServiceUrl }
11
21
12
22
if ( service ?. stat ?. status === 'invalid' ) {
13
23
return { ...parsedService , numberOfPins : - 1 , online : false }
14
24
}
15
25
16
26
const numberOfPins = service . stat ?. pinCount ?. pinned
17
27
const online = typeof numberOfPins === 'number'
28
+ const autoUpload = online ? await mfsPolicyEnableFlag ( service . service , ipfs ) : undefined
18
29
19
- return { ...parsedService , numberOfPins, online }
30
+ return { ...parsedService , numberOfPins, online, autoUpload }
20
31
}
21
32
22
33
const mfsPolicyEnableFlag = memoize ( async ( serviceName , ipfs ) => {
@@ -43,35 +54,26 @@ const uniqueCidBatches = (arrayOfCids, size) => {
43
54
return result
44
55
}
45
56
46
- /**
47
- * TODO: This might change, current version from: https://github.com/ipfs/go-ipfs/blob/petar/pincli/core/commands/remotepin.go#L53
48
- * @typedef {Object } RemotePin
49
- * @property {string } id
50
- * @property {string } name
51
- * @property {('queued'|'pinning'|'pinned'|'failed') } status
52
- * @property {string } cid
53
- * @property {Array<string> } [delegates] (multiaddrs endind with /p2p/peerid)
54
- */
55
57
const pinningBundle = {
56
58
name : 'pinning' ,
59
+ persistActions : [ 'UPDATE_REMOTE_PINS' ] ,
57
60
reducer : ( state = {
58
61
pinningServices : [ ] ,
59
62
remotePins : [ ] ,
60
63
notRemotePins : [ ] ,
61
64
arePinningServicesSupported : false
62
65
} , action ) => {
63
- if ( action . type === 'CACHE_REMOTE_PINS' ) {
64
- const { adds, removals } = action . payload
65
- const remotePins = [ ...state . remotePins , ...adds ] . filter ( p => ! removals . some ( r => r === p . id ) )
66
- const notRemotePins = [ ...state . notRemotePins , ...removals ] . filter ( rid => ! adds . some ( a => a . id === rid ) )
66
+ if ( action . type === 'UPDATE_REMOTE_PINS' ) {
67
+ const { adds = [ ] , removals = [ ] } = action . payload
68
+ const uniq = ( arr ) => [ ...new Set ( arr ) ]
69
+ const remotePins = uniq ( [ ...state . remotePins , ...adds ] . filter ( p => ! removals . some ( r => r === p ) ) )
70
+ const notRemotePins = uniq ( [ ...state . notRemotePins , ...removals ] . filter ( p => ! adds . some ( a => a === p ) ) )
67
71
return { ...state , remotePins, notRemotePins }
68
72
}
69
73
if ( action . type === 'SET_REMOTE_PINNING_SERVICES' ) {
70
74
const oldServices = state . pinningServices
71
75
const newServices = action . payload
72
76
// Skip update when list length did not change and new one has no stats
73
- // so there is no janky update in 'Set pinning modal' when 3+ services
74
- // are defined and some of them are offline.
75
77
if ( oldServices . length === newServices . length ) {
76
78
const withPinStats = s => ( s && typeof s . numberOfPins !== 'undefined' )
77
79
const oldStats = oldServices . some ( withPinStats )
@@ -86,73 +88,74 @@ const pinningBundle = {
86
88
return state
87
89
} ,
88
90
89
- doFetchRemotePins : ( files ) => async ( { dispatch, store, getIpfs } ) => {
90
- // Only check services that are confirmed to be online
91
- const pinningServices = store . selectPinningServices ( ) . filter ( s => s . online )
92
-
91
+ doFetchRemotePins : ( files , skipCache = false ) => async ( { dispatch, store, getIpfs } ) => {
92
+ const pinningServices = store . selectPinningServices ( )
93
93
if ( ! pinningServices ?. length ) return
94
-
95
94
const ipfs = getIpfs ( )
96
-
97
95
if ( ! ipfs || store ?. ipfs ?. ipfs ?. ready || ! ipfs . pin . remote ) return
98
96
99
- const allCids = files ? files . map ( f => f . cid ) : [ ]
97
+ const allCids = files ? files . map ( f => f . cid . toString ( ) ) : [ ]
100
98
101
99
// Reuse known state for some CIDs to avoid unnecessary requests
102
- const cacheId2Cid = ( id ) => id . split ( ':' ) . slice ( - 1 ) [ 0 ]
103
- const remotePins = store . selectRemotePins ( ) . map ( pin => pin . id )
100
+ const remotePins = store . selectRemotePins ( )
104
101
const notRemotePins = store . selectNotRemotePins ( )
105
102
106
- // Check remaining CID status in chunks of 10 (remote API limitation)
107
- const cids = uniqueCidBatches ( allCids , 10 )
103
+ // Check remaining CID status in chunks based on API limitation seen in real world
104
+ const cids = uniqueCidBatches ( allCids , CID_PIN_CHECK_BATCH_SIZE )
108
105
109
106
const adds = [ ]
110
107
const removals = [ ]
111
108
112
109
await Promise . allSettled ( pinningServices . map ( async service => {
113
110
try {
114
111
// skip CIDs that we know the state of at this service
115
- const skipCids = new Set (
112
+ const skipCids = skipCache ? new Set ( ) : new Set (
116
113
[ ...remotePins , ...notRemotePins ]
117
114
. filter ( id => id . startsWith ( service . name ) )
118
115
. map ( cacheId2Cid )
119
116
)
120
- return Promise . allSettled ( cids . map ( async cidChunk => {
117
+ for ( const cidChunk of cids ) {
121
118
const cidsToCheck = cidChunk . filter ( cid => ! skipCids . has ( cid . toString ( ) ) )
122
- if ( ! cidsToCheck . length ) return // skip if no new cids to check
119
+ if ( ! cidsToCheck . length ) continue // skip if no new cids to check
123
120
const notPins = new Set ( cidsToCheck . map ( cid => cid . toString ( ) ) )
124
- const pins = ipfs . pin . remote . ls ( { service : service . name , cid : cidsToCheck } )
125
- for await ( const pin of pins ) {
126
- const pinCid = pin . cid . toString ( )
127
- notPins . delete ( pinCid )
128
- adds . push ( { id : `${ service . name } :${ pinCid } ` , ...pin } )
121
+ try {
122
+ /* TODO: wrap pin.remote.*calls with progressive backoff when response Type == "error" and Message includes "429 Too Many Requests"
123
+ * and see if we could make go-ipfs include Retry-After header in payload description for this type of error */
124
+ const pins = ipfs . pin . remote . ls ( { service : service . name , cid : cidsToCheck . map ( cid => new CID ( cid ) ) } )
125
+ for await ( const pin of pins ) {
126
+ const pinCid = pin . cid . toString ( )
127
+ notPins . delete ( pinCid )
128
+ adds . push ( `${ service . name } :${ pinCid } ` )
129
+ }
130
+ // store 'not pinned remotely on this service' to avoid future checks
131
+ } catch ( e ) {
132
+ console . error ( `Error: pin.remote.ls service=${ service . name } cid=${ cidsToCheck } : ${ e . toString ( ) } ` )
129
133
}
130
- // store 'not pinned remotely on this service' to avoid future checks
134
+ // cache remaining ones as not pinned
131
135
for ( const notPinCid of notPins ) {
132
136
removals . push ( `${ service . name } :${ notPinCid } ` )
133
137
}
134
- } ) )
138
+ }
135
139
} catch ( e ) {
136
140
// ignore service and network errors for now
137
141
// and continue checking remaining ones
138
142
console . error ( 'unexpected error during doFetchRemotePins' , e )
139
143
}
140
144
} ) )
141
- dispatch ( { type : 'CACHE_REMOTE_PINS ' , payload : { adds, removals } } )
145
+ dispatch ( { type : 'UPDATE_REMOTE_PINS ' , payload : { adds, removals } } )
142
146
} ,
143
147
144
148
selectRemotePins : ( state ) => state . pinning . remotePins || [ ] ,
145
149
selectNotRemotePins : ( state ) => state . pinning . notRemotePins || [ ] ,
146
150
147
151
doSelectRemotePinsForFile : ( file ) => ( { store } ) => {
148
152
const pinningServicesNames = store . selectPinningServices ( ) . map ( remote => remote . name )
149
-
150
- const remotePinForFile = store . selectRemotePins ( ) . filter ( pin => pin . cid . string === file . cid . string )
151
- const servicesBeingUsed = remotePinForFile . map ( pin => pin . id . split ( ':' ) [ 0 ] ) . filter ( pinId => pinningServicesNames . includes ( pinId ) )
152
-
153
+ const remotePinForFile = store . selectRemotePins ( ) . filter ( pin => cacheId2Cid ( pin ) === file . cid . toString ( ) )
154
+ const servicesBeingUsed = remotePinForFile . map ( pin => cacheId2ServiceName ( pin ) ) . filter ( name => pinningServicesNames . includes ( name ) )
153
155
return servicesBeingUsed
154
156
} ,
155
157
158
+ // list of services without online check (reads list from config, should be instant)
156
159
doFetchPinningServices : ( ) => async ( { getIpfs, store, dispatch } ) => {
157
160
const ipfs = getIpfs ( )
158
161
if ( ! ipfs || store ?. ipfs ?. ipfs ?. ready || ! ipfs . pin . remote ) return null
@@ -162,14 +165,23 @@ const pinningBundle = {
162
165
if ( ! isPinRemotePresent ) return null
163
166
164
167
const remoteServiceTemplates = store . selectRemoteServiceTemplates ( )
165
- // list of services without online check (should be instant)
166
168
const offlineListOfServices = await ipfs . pin . remote . service . ls ( )
167
169
const remoteServices = await Promise . all ( offlineListOfServices . map ( service => parseService ( service , remoteServiceTemplates , ipfs ) ) )
168
170
dispatch ( { type : 'SET_REMOTE_PINNING_SERVICES' , payload : remoteServices } )
169
- // slower list of services + their pin stats (usually slower)
170
- const fullListOfServices = await ipfs . pin . remote . service . ls ( { stat : true } )
171
- const fullRemoteServices = await Promise . all ( fullListOfServices . map ( service => parseService ( service , remoteServiceTemplates , ipfs ) ) )
172
- dispatch ( { type : 'SET_REMOTE_PINNING_SERVICES' , payload : fullRemoteServices } )
171
+ } ,
172
+
173
+ // fetching pin stats for services is slower/expensive, so we only do that on Settings
174
+ doFetchPinningServicesStats : ( ) => async ( { getIpfs, store, dispatch } ) => {
175
+ const ipfs = getIpfs ( )
176
+ if ( ! ipfs || store ?. ipfs ?. ipfs ?. ready || ! ipfs . pin . remote ) return null
177
+ const isPinRemotePresent = ( await ipfs . commands ( ) ) . Subcommands . find ( c => c . Name === 'pin' ) . Subcommands . some ( c => c . Name === 'remote' )
178
+ if ( ! isPinRemotePresent ) return null
179
+
180
+ const remoteServiceTemplates = store . selectRemoteServiceTemplates ( )
181
+ const servicesWithStats = await ipfs . pin . remote . service . ls ( { stat : true } )
182
+ const remoteServices = await Promise . all ( servicesWithStats . map ( service => parseService ( service , remoteServiceTemplates , ipfs ) ) )
183
+
184
+ dispatch ( { type : 'SET_REMOTE_PINNING_SERVICES' , payload : remoteServices } )
173
185
} ,
174
186
175
187
selectPinningServices : ( state ) => state . pinning . pinningServices || [ ] ,
@@ -186,54 +198,58 @@ const pinningBundle = {
186
198
}
187
199
} ) , { } ) ,
188
200
189
- doSetPinning : ( pin , services = [ ] , wasLocallyPinned , previousRemotePins = [ ] ) => async ( { getIpfs, store, dispatch } ) => {
201
+ doSetPinning : ( file , services = [ ] , wasLocallyPinned , previousRemotePins = [ ] ) => async ( { getIpfs, store, dispatch } ) => {
190
202
const ipfs = getIpfs ( )
191
- const { cid, name } = pin
203
+ const { cid, name } = file
192
204
193
205
const pinLocally = services . includes ( 'local' )
194
206
if ( wasLocallyPinned !== pinLocally ) {
195
207
try {
196
208
pinLocally ? await ipfs . pin . add ( cid ) : await ipfs . pin . rm ( cid )
197
209
} catch ( e ) {
198
210
console . error ( `unexpected local pin error for ${ cid } (${ name } )` , e )
199
- dispatch ( { type : 'IPFS_PIN_FAILED' } )
211
+ const msgArgs = { serviceName : 'local' , errorMsg : e . toString ( ) }
212
+ dispatch ( { type : 'IPFS_PIN_FAILED' , msgArgs } )
200
213
}
201
214
}
202
215
203
216
const adds = [ ]
204
217
const removals = [ ]
205
218
206
- store . selectPinningServices ( ) . filter ( s => s . online ) . forEach ( async service => {
219
+ store . selectPinningServices ( ) . forEach ( async service => {
207
220
const shouldPin = services . includes ( service . name )
208
221
const wasPinned = previousRemotePins . includes ( service . name )
209
222
if ( wasPinned === shouldPin ) return
210
223
224
+ const id = `${ service . name } :${ cid } `
211
225
try {
212
- const id = `${ service . name } :${ pin . cid } `
213
226
if ( shouldPin ) {
214
- adds . push ( { id, ...pin } )
215
- // TODO: remove background:true and add pin job to queue.
216
- // wait for pinning to finish + add indicator for ongoing pinning
227
+ adds . push ( id )
228
+ /* TODO: remove background:true below and add pin job to persisted queue.
229
+ * We want track ongoing pinning across restarts of webui/ipfs-desktop
230
+ * See: https://github.com/ipfs/ipfs-webui/issues/1752 */
217
231
await ipfs . pin . remote . add ( cid , { service : service . name , name, background : true } )
218
232
} else {
219
233
removals . push ( id )
220
234
await ipfs . pin . remote . rm ( { cid : [ cid ] , service : service . name } )
221
235
}
222
236
} catch ( e ) {
223
237
// log error and continue with other services
224
- console . error ( `unexpected pin.remote error for ${ cid } @${ service . name } ` , e )
225
- dispatch ( { type : 'IPFS_PIN_FAILED' } )
238
+ console . error ( `ipfs.pin.remote error for ${ cid } @${ service . name } ` , e )
239
+ const msgArgs = { serviceName : service . name , errorMsg : e . toString ( ) }
240
+ dispatch ( { type : 'IPFS_PIN_FAILED' , msgArgs } )
226
241
}
227
242
} )
228
243
229
- dispatch ( { type : 'CACHE_REMOTE_PINS ' , payload : { adds, removals } } )
244
+ dispatch ( { type : 'UPDATE_REMOTE_PINS ' , payload : { adds, removals } } )
230
245
231
246
await store . doPinsFetch ( )
232
247
} ,
233
248
doAddPinningService : ( { apiEndpoint, nickname, secretApiKey } ) => async ( { getIpfs } ) => {
234
249
const ipfs = getIpfs ( )
235
250
236
251
// temporary mitigation for https://github.com/ipfs/ipfs-webui/issues/1770
252
+ // update: still present a year later – i think there is a lesson here :-)
237
253
nickname = nickname . replaceAll ( '.' , '_' )
238
254
239
255
await ipfs . pin . remote . service . add ( nickname , {
0 commit comments