alanshaw / ipfs-only-hash

#️⃣ Just enough code to calculate the IPFS hash for some data
MIT License
135 stars 28 forks source link

Hash a directory #18

Open rrthomas opened 3 years ago

rrthomas commented 3 years ago

It would be great to be able to use this module to do the equivalent of ipfs add -r --only-hash.

rrthomas commented 3 years ago

In fact, this should be very easy to achieve, as if I change:

for await (const { cid } of importer([{ content }], block, options)) {

to

for await (const { cid } of importer(content, block, options)) {

then I can pass in the result of e.g. globsource().

So it seems that all that is needed to keep things as convenient as at the moment is a bit more automatic type-matching code like the current:

  if (typeof content === 'string') {
    content = new TextEncoder().encode(content)
  }
rrthomas commented 3 years ago

After a bit more thought, I can see how to detect whether the argument is a Uint8Array, but not how to distinguish the case where it's an AsyncIterable<Uint8Array> (where we want to lift it into an array containing an object) from an AsyncIterable<ImportCandidate> (where we want to leave it alone).

If it's impossible to make this distinction, then it would be possible instead to add a second API to ipfs-only-hash.

rrthomas commented 2 years ago

For now, I am using the following code, and specifically the ofDir method:

// A slightly modified version of ipfs-only-hash
// See https://github.com/alanshaw/ipfs-only-hash/issues/18
const { globSource } = require('ipfs-http-client');
const { importer } = require('ipfs-unixfs-importer');

const block = {
  get: async (cid) => { throw new Error(`unexpected block API get for ${cid}`); },
  put: async () => { throw new Error('unexpected block API put'); },
};

async function hash(content_, options_) {
  const options = options_ || {};
  options.onlyHash = true;

  let content = content_;
  if (typeof content === 'string') {
    content = [{ content: new TextEncoder().encode(content) }];
  } else if (content instanceof Object.getPrototypeOf(Uint8Array)) {
    content = [{ content }];
  }

  let lastCID;
  for await (const { cid } of importer(content, block, options)) {
    lastCID = cid;
  }
  return lastCID;
}

module.exports = {
  cidToHex(cid) {
    return `0x${Buffer.from(cid.bytes.slice(2)).toString('hex')}`;
  },

  of: hash,

  async ofDir(directory) {
    const options = {
      cidVersion: 0, // Lines up with the smart contract code
    };

    const files = globSource(directory, { recursive: true });
    const rootCID = await hash(files, options);
    return rootCID;
  },
};
wilfredjonathanjames commented 1 year ago

Updated version of @rrthomas's code. Note ofFile is distinct to ofDir, as it doesn't wrap the file in a directory.

Not perfect but good enough to get started with.

// A slightly modified version of ipfs-only-hash
// See https://github.com/alanshaw/ipfs-only-hash/issues/18

const block = {
  get: async (cid) => {
    throw new Error(`unexpected block API get for ${cid}`);
  },
  put: async () => {
    throw new Error('unexpected block API put');
  },
};

async function hash(content_, options_) {
  const { importer } = await import('ipfs-unixfs-importer');
  const options = options_ || {};
  options.onlyHash = true;

  let content = content_;
  if (typeof content === 'string') {
    content = [{ content: new TextEncoder().encode(content) }];
  } else if (content instanceof Object.getPrototypeOf(Uint8Array)) {
    content = [{ content }];
  }

  let lastCID;
  for await (const c of importer(content, block, options)) {
    lastCID = c.cid;
  }
  return lastCID;
}

async function hashFiles(path, options) {
  const { globSource } = await import('ipfs-http-client');
  options = {
    cidVersion: 0, // Lines up with the smart contract code
    hidden: true,
    ...options,
  };

  const files = globSource(path, '**');

  const rootCID = await hash(files, options);
  return rootCID;
}

module.exports = {
  cidToHex(cid) {
    return `0x${Buffer.from(cid.bytes.slice(2)).toString('hex')}`;
  },

  of: hash,

  async ofFile(path) {
    return await hashFiles(path, {});
  },

  async ofDir(path) {
    return await hashFiles(path, {
      wrapWithDirectory: true,
    });
  },
};
rrthomas commented 1 year ago

@wjagodfrey Thanks for this fix! I will use it myself.