1717package client
1818
1919import (
20+ "bytes"
2021 "encoding/json"
2122 "fmt"
2223 "io"
@@ -27,6 +28,7 @@ import (
2728 "path/filepath"
2829 "strings"
2930
31+ "github.com/containerd/stargz-snapshotter/ipfs/ipnskey"
3032 "github.com/mitchellh/go-homedir"
3133 ma "github.com/multiformats/go-multiaddr"
3234 manet "github.com/multiformats/go-multiaddr/net"
@@ -226,3 +228,243 @@ func GetIPFSAPIAddress(ipfsPath string, scheme string) (string, error) {
226228 }
227229 return iurl , nil
228230}
231+
232+ // Resolve resolves the IPNS name to its corresponding CID.
233+ func (c * Client ) Resolve (ref string ) (string , error ) {
234+ if c .Address == "" {
235+ return "" , fmt .Errorf ("specify IPFS API address" )
236+ }
237+
238+ peerID , err := c .importKey (ref )
239+ if err != nil {
240+ return "" , fmt .Errorf ("failed to import key: %w" , err )
241+ }
242+
243+ client := c .Client
244+ if client == nil {
245+ client = http .DefaultClient
246+ }
247+
248+ ipfsAPINameResolve := c .Address + "/api/v0/name/resolve"
249+ req , err := http .NewRequest ("POST" , ipfsAPINameResolve , nil )
250+ if err != nil {
251+ return "" , err
252+ }
253+
254+ q := req .URL .Query ()
255+ q .Add ("arg" , "/ipns/" + peerID )
256+ q .Add ("nocache" , "true" )
257+ req .URL .RawQuery = q .Encode ()
258+
259+ resp , err := client .Do (req )
260+ if err != nil {
261+ return "" , err
262+ }
263+ defer func () {
264+ io .Copy (io .Discard , resp .Body )
265+ resp .Body .Close ()
266+ }()
267+
268+ if resp .StatusCode / 100 != 2 {
269+ return "" , fmt .Errorf ("failed to resolve name %v; status code: %v" , peerID , resp .StatusCode )
270+ }
271+
272+ // rs represents the information provided by "/api/v0/name/resolve" API of IPFS.
273+ // Please see details at: https://docs.ipfs.tech/reference/kubo/rpc/#api-v0-name-resolve
274+ var rs struct {
275+ Path string `json:"Path"`
276+ }
277+ if err := json .NewDecoder (resp .Body ).Decode (& rs ); err != nil {
278+ return "" , err
279+ }
280+
281+ parts := strings .Split (rs .Path , "/" )
282+ if len (parts ) < 3 || parts [1 ] != "ipfs" {
283+ return "" , fmt .Errorf ("invalid resolved path format: %s" , rs .Path )
284+ }
285+
286+ // This is compatible to IPFS behaviour: https://docs.ipfs.tech/concepts/ipns/#ipns-keys
287+ return parts [2 ], nil
288+ }
289+
290+ // Publish publishes the given CID to IPNS using the key associated with the given ref.
291+ // Please see details at: https://docs.ipfs.tech/reference/kubo/rpc/#api-v0-name-publish
292+ func (c * Client ) Publish (ref string , cid string ) error {
293+ if c .Address == "" {
294+ return fmt .Errorf ("specify IPFS API address" )
295+ }
296+
297+ _ , err := c .importKey (ref )
298+ if err != nil {
299+ return fmt .Errorf ("failed to import key: %w" , err )
300+ }
301+
302+ client := c .Client
303+ if client == nil {
304+ client = http .DefaultClient
305+ }
306+
307+ ipfsAPINamePublish := c .Address + "/api/v0/name/publish"
308+ req , err := http .NewRequest ("POST" , ipfsAPINamePublish , nil )
309+ if err != nil {
310+ return err
311+ }
312+
313+ q := req .URL .Query ()
314+ q .Add ("arg" , "/ipfs/" + cid )
315+ q .Add ("key" , ref )
316+ q .Add ("allow-offline" , "true" )
317+ req .URL .RawQuery = q .Encode ()
318+
319+ resp , err := client .Do (req )
320+ if err != nil {
321+ return err
322+ }
323+ defer func () {
324+ io .Copy (io .Discard , resp .Body )
325+ resp .Body .Close ()
326+ }()
327+
328+ respBody , err := io .ReadAll (resp .Body )
329+ if err != nil {
330+ return fmt .Errorf ("failed to read response body: %v" , err )
331+ }
332+
333+ if resp .StatusCode / 100 != 2 {
334+ return fmt .Errorf ("failed to publish; status code: %v, body: %s\n " +
335+ "Request URL: %s" , resp .StatusCode , string (respBody ), ipfsAPINamePublish )
336+ }
337+
338+ return nil
339+ }
340+
341+ // importKey imports the key pair associated with the given ref into the local IPFS node.
342+ // The ref will be used as the key name in IPFS. If the key already exists, it will return nil.
343+ // Please see details at: https://docs.ipfs.tech/reference/kubo/rpc/#api-v0-key-import
344+ func (c * Client ) importKey (ref string ) (string , error ) {
345+ if c .Address == "" {
346+ return "" , fmt .Errorf ("specify IPFS API address" )
347+ }
348+
349+ keyID , err := c .getKeyIDFromIPFS (ref )
350+ if err == nil && keyID != "" {
351+ return keyID , nil
352+ }
353+
354+ keyData , err := ipnskey .GenerateKeyData (ref )
355+ if err != nil {
356+ return "" , fmt .Errorf ("failed to generate key data: %w" , err )
357+ }
358+
359+ body := & bytes.Buffer {}
360+ writer := multipart .NewWriter (body )
361+
362+ safeFilename := strings .ReplaceAll (ref , "/" , "_" )
363+ safeFilename = strings .ReplaceAll (safeFilename , ":" , "_" )
364+
365+ part , err := writer .CreateFormFile ("file" , safeFilename + ".pem" )
366+ if err != nil {
367+ return "" , fmt .Errorf ("failed to create form file: %v" , err )
368+ }
369+
370+ _ , err = part .Write (keyData )
371+ if err != nil {
372+ return "" , fmt .Errorf ("failed to write key data: %v" , err )
373+ }
374+
375+ err = writer .Close ()
376+ if err != nil {
377+ return "" , fmt .Errorf ("failed to close multipart writer: %v" , err )
378+ }
379+
380+ encodedKeyname := url .QueryEscape (ref )
381+ ipfsAPIKeyImport := fmt .Sprintf ("%s/api/v0/key/import?arg=%s&format=pem-pkcs8-cleartext" , c .Address , encodedKeyname )
382+
383+ req , err := http .NewRequest ("POST" , ipfsAPIKeyImport , body )
384+ if err != nil {
385+ return "" , fmt .Errorf ("failed to create HTTP request: %v" , err )
386+ }
387+
388+ req .Header .Set ("Content-Type" , writer .FormDataContentType ())
389+
390+ client := c .Client
391+ if client == nil {
392+ client = http .DefaultClient
393+ }
394+
395+ resp , err := client .Do (req )
396+ if err != nil {
397+ return "" , fmt .Errorf ("failed to send request: %v" , err )
398+ }
399+ defer resp .Body .Close ()
400+
401+ respBody , err := io .ReadAll (resp .Body )
402+ if err != nil {
403+ return "" , fmt .Errorf ("failed to read response body: %v" , err )
404+ }
405+
406+ if resp .StatusCode != http .StatusOK {
407+ return "" , fmt .Errorf ("IPFS API returned error status: %d, body: %s\n Request URL: %s" , resp .StatusCode , string (respBody ), ipfsAPIKeyImport )
408+ }
409+
410+ return c .getKeyIDFromIPFS (ref )
411+ }
412+
413+ // getKeyIDFromIPFS checks if a key with the given name already exists in IPFS
414+ func (c * Client ) getKeyIDFromIPFS (name string ) (string , error ) {
415+ client := c .Client
416+ if client == nil {
417+ client = http .DefaultClient
418+ }
419+
420+ ipfsAPIKeyList := c .Address + "/api/v0/key/list"
421+ req , err := http .NewRequest ("POST" , ipfsAPIKeyList , nil )
422+ if err != nil {
423+ return "" , err
424+ }
425+
426+ resp , err := client .Do (req )
427+ if err != nil {
428+ return "" , fmt .Errorf ("failed to get key list: %v" , err )
429+ }
430+ defer resp .Body .Close ()
431+
432+ respBody , err := io .ReadAll (resp .Body )
433+ if err != nil {
434+ return "" , fmt .Errorf ("failed to read response body: %v" , err )
435+ }
436+
437+ if resp .StatusCode != http .StatusOK {
438+ return "" , fmt .Errorf ("IPFS API returned error status: %d, body: %s\n Request URL: %s" , resp .StatusCode , string (respBody ), ipfsAPIKeyList )
439+ }
440+
441+ var result struct {
442+ Keys []struct {
443+ Name string `json:"name"`
444+ ID string `json:"id"`
445+ } `json:"Keys"`
446+ }
447+
448+ if err := json .Unmarshal (respBody , & result ); err != nil {
449+ return "" , fmt .Errorf ("failed to decode response: %v" , err )
450+ }
451+
452+ for _ , key := range result .Keys {
453+ if key .Name == name {
454+ return key .ID , nil
455+ }
456+ }
457+
458+ return "" , fmt .Errorf ("key not found: %s" , name )
459+ }
460+
461+ func (c * Client ) IsRef (s string ) bool {
462+ parts := strings .Split (s , "/" )
463+ lastPart := parts [len (parts )- 1 ]
464+
465+ if strings .Contains (lastPart , ":" ) || strings .Contains (lastPart , "@" ) {
466+ return true
467+ }
468+
469+ return len (parts ) >= 2
470+ }
0 commit comments