@@ -16,13 +16,15 @@ import (
1616 "time"
1717
1818 humanize "github.com/dustin/go-humanize"
19+ oldcmds "github.com/ipfs/kubo/commands"
1920 "github.com/ipfs/kubo/config"
2021 "github.com/ipfs/kubo/core"
2122 "github.com/ipfs/kubo/core/commands/cmdenv"
2223 "github.com/ipfs/kubo/core/node"
2324 fsrepo "github.com/ipfs/kubo/repo/fsrepo"
2425
2526 bservice "github.com/ipfs/boxo/blockservice"
27+ bstore "github.com/ipfs/boxo/blockstore"
2628 offline "github.com/ipfs/boxo/exchange/offline"
2729 dag "github.com/ipfs/boxo/ipld/merkledag"
2830 ft "github.com/ipfs/boxo/ipld/unixfs"
@@ -31,7 +33,6 @@ import (
3133 cid "github.com/ipfs/go-cid"
3234 cidenc "github.com/ipfs/go-cidutil/cidenc"
3335 "github.com/ipfs/go-datastore"
34- fslock "github.com/ipfs/go-fs-lock"
3536 cmds "github.com/ipfs/go-ipfs-cmds"
3637 ipld "github.com/ipfs/go-ipld-format"
3738 logging "github.com/ipfs/go-log/v2"
@@ -124,19 +125,19 @@ performance.`,
124125 cmds .BoolOption (filesFlushOptionName , "f" , "Flush target and ancestors after write." ).WithDefault (true ),
125126 },
126127 Subcommands : map [string ]* cmds.Command {
127- "read" : filesReadCmd ,
128- "write" : filesWriteCmd ,
129- "mv" : filesMvCmd ,
130- "cp" : filesCpCmd ,
131- "ls" : filesLsCmd ,
132- "mkdir" : filesMkdirCmd ,
133- "stat" : filesStatCmd ,
134- "rm" : filesRmCmd ,
135- "flush" : filesFlushCmd ,
136- "chcid" : filesChcidCmd ,
137- "chmod" : filesChmodCmd ,
138- "touch " : filesTouchCmd ,
139- "replace-root " : filesReplaceRoot ,
128+ "read" : filesReadCmd ,
129+ "write" : filesWriteCmd ,
130+ "mv" : filesMvCmd ,
131+ "cp" : filesCpCmd ,
132+ "ls" : filesLsCmd ,
133+ "mkdir" : filesMkdirCmd ,
134+ "stat" : filesStatCmd ,
135+ "rm" : filesRmCmd ,
136+ "flush" : filesFlushCmd ,
137+ "chcid" : filesChcidCmd ,
138+ "chmod" : filesChmodCmd ,
139+ "chroot " : filesChrootCmd ,
140+ "touch " : filesTouchCmd ,
140141 },
141142}
142143
@@ -1366,111 +1367,6 @@ Remove files or directories.
13661367 },
13671368}
13681369
1369- var filesReplaceRoot = & cmds.Command {
1370- Helptext : cmds.HelpText {
1371- Tagline : "Replace the MFS root." ,
1372- ShortDescription : `
1373- Replace the filesystem root with another CID when the filesystem. Usually used as recovery method if MFS has been corrupted.
1374- ` ,
1375- LongDescription : `
1376- Replace the filesystem root with another CID.
1377- This command is meant to run in standalone mode when the daemon isn't running.
1378- It is an advanced command that you normally do *not* want to run except when
1379- the filesystem has been corrupted and the daemon refuses to run.
1380-
1381- FIXME: Add an example of the daemon not running once https://github.com/ipfs/go-ipfs/issues/7183
1382- is resolved.
1383-
1384- $ ipfs init
1385- [...]
1386- ipfs cat /ipfs/QmQPeNsJPyVWPFDVHb77w8G42Fvo15z4bG2X8D2GhfbSXc/readme # <- init dir
1387- [...]
1388- $ ipfs files ls /
1389- [nothing; empty dir]
1390- $ ipfs files stat / --hash
1391- QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn
1392- # FIXME(BLOCKING): Need the following to somehow "start" the root dir, otherwise
1393- # the replace-root will fail to find the '/local/filesroot' entry in the repo
1394- $ ipfs files cp /ipfs/QmQPeNsJPyVWPFDVHb77w8G42Fvo15z4bG2X8D2GhfbSXc/readme /file
1395- $ GOLOG_LOG_LEVEL="info" ipfs files replace-root QmQPeNsJPyVWPFDVHb77w8G42Fvo15z4bG2X8D2GhfbSXc # init dir from before
1396- [...] replaced MFS files root QmQPeNsJPyVWPFDVHb77w8G42Fvo15z4bG2X8D2GhfbSXc [...]
1397- [here we have the CID of the old root to "undo" in case of error]
1398- $ ipfs files ls /
1399- [contents from init dir now set as the root of the filesystem]
1400- about
1401- contact
1402- help
1403- ping
1404- quick-start
1405- readme
1406- security-notes
1407- $ ipfs files replace-root QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn # empty dir from init
1408- $ ipfs files ls /
1409- [nothing; empty dir]
1410- ` ,
1411- },
1412- Arguments : []cmds.Argument {
1413- cmds .StringArg ("new-root" , true , false , "New root to use." ),
1414- },
1415- // FIXME(BLOCKING): Can/should we do this with the repo running?
1416- NoRemote : true ,
1417- Run : func (req * cmds.Request , res cmds.ResponseEmitter , env cmds.Environment ) error {
1418- if len (req .Arguments ) < 1 {
1419- return fmt .Errorf ("new root not provided" )
1420- }
1421- newFilesRootCid , err := cid .Parse (req .Arguments [0 ])
1422- if err != nil {
1423- return fmt .Errorf ("files root argument provided %s is not a valid CID: %w" , req .Arguments [0 ], err )
1424-
1425- }
1426- // FIXME(BLOCKING): Check (a) that this CID exists *locally* and (b)
1427- // that it's a dir.
1428-
1429- cfgRoot , err := cmdenv .GetConfigRoot (env )
1430- if err != nil {
1431- return err
1432- }
1433-
1434- repo , err := fsrepo .Open (cfgRoot )
1435- if err != nil {
1436- if pathError , ok := err .(* os.PathError ); ok {
1437- if _ , isLockError := pathError .Unwrap ().(fslock.LockedError ); isLockError {
1438- return fmt .Errorf ("aquiring the repo lock (make sure the daemon isn't running): %w" , err )
1439- }
1440- }
1441- return err
1442- }
1443- localDS := repo .Datastore ()
1444- defer repo .Close ()
1445-
1446- filesRootBytes , err := localDS .Get (req .Context , node .FilesRootDatastoreKey )
1447- if err == datastore .ErrNotFound {
1448- return fmt .Errorf ("MFS files root %s not found in repo" , node .FilesRootDatastoreKey )
1449- } else if err != nil {
1450- return fmt .Errorf ("looking for MFS files root: %w" , err )
1451- }
1452- filesRootCid , err := cid .Cast (filesRootBytes )
1453- if err != nil {
1454- return fmt .Errorf ("casting MFS files root %s as CID: %w" , filesRootBytes , err )
1455- }
1456-
1457- err = localDS .Put (req .Context , node .FilesRootDatastoreKey , newFilesRootCid .Bytes ())
1458- if err != nil {
1459- return fmt .Errorf ("storing new files root: %w" , err )
1460- }
1461- // FIXME(BLOCKING): Do we need this if we're closing the repo at the end
1462- // of the command? Likely not.
1463- err = localDS .Sync (req .Context , node .FilesRootDatastoreKey )
1464- if err != nil {
1465- return fmt .Errorf ("syncing new files root: %w" , err )
1466- }
1467-
1468- log .Infof ("replaced MFS files root %s with %s" , filesRootCid , newFilesRootCid )
1469-
1470- return nil
1471- },
1472- }
1473-
14741370func removePath (filesRoot * mfs.Root , path string , force bool , dashr bool ) error {
14751371 if path == "/" {
14761372 return fmt .Errorf ("cannot delete root" )
@@ -1758,3 +1654,153 @@ Examples:
17581654 return mfs .Touch (nd .FilesRoot , path , ts )
17591655 },
17601656}
1657+
1658+ const chrootConfirmOptionName = "confirm"
1659+
1660+ var filesChrootCmd = & cmds.Command {
1661+ Status : cmds .Experimental ,
1662+ Helptext : cmds.HelpText {
1663+ Tagline : "Change the MFS root CID." ,
1664+ ShortDescription : `
1665+ 'ipfs files chroot' changes the root CID used by MFS (Mutable File System).
1666+ This is a recovery command for when MFS becomes corrupted and prevents the
1667+ daemon from starting.
1668+
1669+ When run without a CID argument, resets MFS to an empty directory.
1670+
1671+ WARNING: The old MFS root and its unpinned children will be removed during
1672+ the next garbage collection. Pin the old root first if you want to preserve.
1673+
1674+ This command can only run when the daemon is not running.
1675+ ` ,
1676+ LongDescription : `
1677+ 'ipfs files chroot' changes the root CID used by MFS (Mutable File System).
1678+ This is a recovery command for when MFS becomes corrupted and prevents the
1679+ daemon from starting.
1680+
1681+ When run without a CID argument, resets MFS to an empty directory.
1682+
1683+ WARNING: The old MFS root and its unpinned children will be removed during
1684+ the next garbage collection. Pin the old root first if you want to preserve.
1685+
1686+ This command can only run when the daemon is not running.
1687+
1688+ Examples:
1689+
1690+ # Reset MFS to empty directory (recovery from corruption)
1691+ $ ipfs files chroot --confirm
1692+
1693+ # Restore MFS to a known good directory CID
1694+ $ ipfs files chroot --confirm QmYourBackupCID
1695+ ` ,
1696+ },
1697+ Arguments : []cmds.Argument {
1698+ cmds .StringArg ("cid" , false , false , "New root CID (defaults to empty directory if not specified)." ),
1699+ },
1700+ Options : []cmds.Option {
1701+ cmds .BoolOption (chrootConfirmOptionName , "Confirm this potentially destructive operation." ),
1702+ },
1703+ NoRemote : true ,
1704+ Extra : CreateCmdExtras (SetDoesNotUseRepo (true )),
1705+ Run : func (req * cmds.Request , res cmds.ResponseEmitter , env cmds.Environment ) error {
1706+ confirm , _ := req .Options [chrootConfirmOptionName ].(bool )
1707+ if ! confirm {
1708+ return errors .New ("this is a potentially destructive operation; pass --confirm to proceed" )
1709+ }
1710+
1711+ // Determine new root CID
1712+ var newRootCid cid.Cid
1713+ if len (req .Arguments ) > 0 {
1714+ var err error
1715+ newRootCid , err = cid .Decode (req .Arguments [0 ])
1716+ if err != nil {
1717+ return fmt .Errorf ("invalid CID %q: %w" , req .Arguments [0 ], err )
1718+ }
1719+ } else {
1720+ // Default to empty directory
1721+ newRootCid = ft .EmptyDirNode ().Cid ()
1722+ }
1723+
1724+ // Get config root to open repo directly
1725+ cctx := env .(* oldcmds.Context )
1726+ cfgRoot := cctx .ConfigRoot
1727+
1728+ // Open repo directly (daemon must not be running)
1729+ repo , err := fsrepo .Open (cfgRoot )
1730+ if err != nil {
1731+ return fmt .Errorf ("opening repo (is the daemon running?): %w" , err )
1732+ }
1733+ defer repo .Close ()
1734+
1735+ localDS := repo .Datastore ()
1736+ bs := bstore .NewBlockstore (localDS )
1737+
1738+ // Check new root exists locally and is a directory
1739+ hasBlock , err := bs .Has (req .Context , newRootCid )
1740+ if err != nil {
1741+ return fmt .Errorf ("checking if new root exists: %w" , err )
1742+ }
1743+ if ! hasBlock {
1744+ // Special case: empty dir is always available (hardcoded in boxo)
1745+ emptyDirCid := ft .EmptyDirNode ().Cid ()
1746+ if ! newRootCid .Equals (emptyDirCid ) {
1747+ return fmt .Errorf ("new root %s does not exist locally; fetch it first with 'ipfs block get'" , newRootCid )
1748+ }
1749+ }
1750+
1751+ // Validate it's a directory (not a file)
1752+ if hasBlock {
1753+ blk , err := bs .Get (req .Context , newRootCid )
1754+ if err != nil {
1755+ return fmt .Errorf ("reading new root block: %w" , err )
1756+ }
1757+ pbNode , err := dag .DecodeProtobuf (blk .RawData ())
1758+ if err != nil {
1759+ return fmt .Errorf ("new root is not a valid dag-pb node: %w" , err )
1760+ }
1761+ fsNode , err := ft .FSNodeFromBytes (pbNode .Data ())
1762+ if err != nil {
1763+ return fmt .Errorf ("new root is not a valid UnixFS node: %w" , err )
1764+ }
1765+ if fsNode .Type () != ft .TDirectory && fsNode .Type () != ft .THAMTShard {
1766+ return fmt .Errorf ("new root must be a directory, got %s" , fsNode .Type ())
1767+ }
1768+ }
1769+
1770+ // Get old root for display (if exists)
1771+ var oldRootStr string
1772+ oldRootBytes , err := localDS .Get (req .Context , node .FilesRootDatastoreKey )
1773+ if err == nil {
1774+ oldRootCid , err := cid .Cast (oldRootBytes )
1775+ if err == nil {
1776+ oldRootStr = oldRootCid .String ()
1777+ }
1778+ } else if ! errors .Is (err , datastore .ErrNotFound ) {
1779+ return fmt .Errorf ("reading current MFS root: %w" , err )
1780+ }
1781+
1782+ // Write new root
1783+ err = localDS .Put (req .Context , node .FilesRootDatastoreKey , newRootCid .Bytes ())
1784+ if err != nil {
1785+ return fmt .Errorf ("writing new MFS root: %w" , err )
1786+ }
1787+
1788+ // Build output message
1789+ var msg string
1790+ if oldRootStr != "" {
1791+ msg = fmt .Sprintf ("MFS root changed from %s to %s\n " , oldRootStr , newRootCid )
1792+ msg += fmt .Sprintf ("The old root %s will be garbage collected unless pinned.\n " , oldRootStr )
1793+ } else {
1794+ msg = fmt .Sprintf ("MFS root set to %s\n " , newRootCid )
1795+ }
1796+
1797+ return cmds .EmitOnce (res , & MessageOutput {Message : msg })
1798+ },
1799+ Type : MessageOutput {},
1800+ Encoders : cmds.EncoderMap {
1801+ cmds .Text : cmds .MakeTypedEncoder (func (req * cmds.Request , w io.Writer , out * MessageOutput ) error {
1802+ _ , err := fmt .Fprint (w , out .Message )
1803+ return err
1804+ }),
1805+ },
1806+ }
0 commit comments