packr icon indicating copy to clipboard operation
packr copied to clipboard

packr2: allow mixed virtual + file system boxes

Open dragonsinth opened this issue 6 years ago • 10 comments

packr is setup such that a box is purely virtual or purely file system, and virtual is always preferred over file system.

In our environment, we need the opposite: our asset compilation pipeline is fairly complex, and run only by the engineers who work on the service. Those engineers need the file system to dominate, since it represents their in-progress work. Afterwards they regenerate and commit updated packr boxes, but not any of the intermediate or compiled on-disk assets. This allows everyone else (who isn't working on that service) to just run the service without doing any asset compilation themselves.

I have a fairly clean patch (I think) to support this that I would like to submit, if there's a chance it might be accepted?

dragonsinth avatar May 24 '19 18:05 dragonsinth

If I am not mistaken, packr2 offers this exact scenario.

Also, I believe that packr1 is not developed anymore. Is is possible for you to migrate to packr2 ?

davidovich avatar May 24 '19 19:05 davidovich

Ah, I wasn't sure if packr1 was completely "done" or not.

dragonsinth avatar May 24 '19 20:05 dragonsinth

So I played around with this in packr2, and there's no obvious way to make this work out of the box. My initial stab was to try this:

type resolveFunc func(string, string) (file.File, error)

func (f resolveFunc) Resolve(box string, name string) (file.File, error) {
	return f(box, name)
}

func chainResolvers(resolvers ...resolver.Resolver) resolver.Resolver {
	return resolveFunc(func(box string, name string) (file.File, error) {
		var ret file.File
		var err error
		for _, r := range resolvers {
			ret, err = r.Resolve(box, name)
			if err == nil {
				return ret, nil
			}
		}
		return ret, err
	})
}

func LocalDevBox(box *packr.Box) {
	r, ok := box.DefaultResolver.(*resolver.HexGzip)
	if !ok {
		panic(fmt.Sprintf("not a hex gzip box: %T", box.DefaultResolver))
	}
	box.DefaultResolver = chainResolvers(&resolver.Disk{Root: box.ResolutionDir}, r)
}

The problem centers around the special treatment of box.resolvers resolversMap. The construction makes it supremely difficult to do what I'm trying to do -- I don't really grok the reason having different resolvers at each file path.

I think an easier-to-work-with construction would be:

  • generated boxes have a DefaultResolver setup with static data
  • get rid of box.resolvers
  • make Resolver implement FileMap

If we did this, it would be easy to chain / layer resolvers, and the implementation of box.Walk would be much simpler.

dragonsinth avatar May 24 '19 21:05 dragonsinth

I'm not against a PR on this. My biggest question is is it a breaking change? if so, what does that look like?

markbates avatar May 24 '19 22:05 markbates

Good question-- is the ability to set a different resolver for per path an important design choice, or an implementation detail for constructing pointer boxes?

I've been looking through packr2 today, and there seems to be a lot more there than packr1, but I haven't yet been able to distill out the design philosophy for packr2. Do you have something that explains packr2 vs. packr1, in terms of what packr2 is trying to accomplish?

dragonsinth avatar May 25 '19 04:05 dragonsinth

It was, and is, import to be able to resolve a "file" a different way than others. for example, a CMS is using it so they can have some files local, and others in a DB/cache.

Anyway, I can give you more details, but you get the idea.

I think we're definitely talking a breaking change, but one that could be done as a v3, as I quite like the idea.

My thoughts for would this pattern would look like is something like this:

type ResolverBox interface {
	packd.Box
	Resolve(box string, file string) (file.File, error)
}

func WithResolver(b ResolverBox, boxName string, file string, res resolver.Resolver) ResolverBox {
	rb := resolverBox{
		ResolverBox: b,
		boxName:     boxName,
		file:        file,
		res:         res,
	}
	return rb
}

type resolverBox struct {
	ResolverBox
	boxName string
	file    string
	res     resolver.Resolver
}

func (b resolverBox) Resolve(box string, file string) (file.File, error) {
	if box == b.boxName && file == b.file {
		return b.res.Resolve(box, file)
	}
	return b.ResolverBox.Resolve(box, file)
}

that's a quick thought on the subject.

any interest in working up a proposal/PR on this?

markbates avatar May 26 '19 21:05 markbates

Ignore my previous bad code idea. A box could have a single resolver and you can just wrap it like middleware On my phone or I’d type it out. But basically what you’re were going for.

markbates avatar May 27 '19 00:05 markbates

Been stewing on this and I think I may have a backwards compatible idea that retains the ability to do file-specific resolution. Seems like the core of Box could be something like:

type Box struct {
	Path       string
	Name       string
	resolvers []resolver.Resolver // resolvers in priority order
}

resolvers would be an ordered list of resolvers to try. Resolver would have to do slightly more-- more of what's in Box would probably be delegated. For example, implementing Walk would probably entail all Resolvers implementing FileMappable, and Box would just merge the results together such that earlier resolver paths would take precedence over later ones.

The upside is that Box gets really clean: at the moment it does a bunch of dirty things like look at disk directly, when in reality it should just have a Disk resolver to delegate these parts to.

AddString / AddBytes could be done by checking if the first resolver in the chain is an in-memory resolver; if so, add to that; if not prepend a new in-memory resolver to the chain first.

What do you think of the general idea?

dragonsinth avatar May 27 '19 01:05 dragonsinth

(A wrap-and-delegate chain could also an effective strategy, although it might be harder to correctly implement resolvers.)

dragonsinth avatar May 27 '19 02:05 dragonsinth

@dragonsinth If I am not mistaken, you could achieve this a bit easier by using spf13/afero‘s CopyOnWriteFS, defining a boxes HttpFS as the read-only part and your OS‘s FS as the overlay part. Did not test that, but I guess it should work, it puts the layer of abstraction where it belongs to (from an actual FS implementation to a dedicated abstraction layer) and it is pretty much done.

mwmahlberg avatar Oct 08 '19 07:10 mwmahlberg