[lxc-devel] [lxd/master] Directory api

tych0 on Github lxc-bot at linuxcontainers.org
Wed Sep 7 22:49:24 UTC 2016


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 301 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20160907/fc116882/attachment.bin>
-------------- next part --------------
From 65029e4c85450fb3624ff61f611ef10ab89522fa Mon Sep 17 00:00:00 2001
From: Tycho Andersen <tycho.andersen at canonical.com>
Date: Fri, 19 Aug 2016 17:47:01 +0000
Subject: [PATCH 1/2] initial implementation of recursive file push/pull

Closes #1218

Signed-off-by: Tycho Andersen <tycho.andersen at canonical.com>
---
 client.go                | 140 +++++++++++++++++++++++++++++++++++++++++++++--
 lxc/file.go              |  70 ++++++++++++++++++++----
 lxd/container.go         |   2 +-
 lxd/container_file.go    |  81 ++++++++++++++++-----------
 lxd/container_lxc.go     |  36 ++++++++----
 lxd/nsexec.go            |  75 +++++++++++++++++++++----
 lxd/response.go          |  11 ++++
 shared/util.go           |  12 +++-
 test/suites/filemanip.sh |  12 ++++
 9 files changed, 365 insertions(+), 74 deletions(-)

diff --git a/client.go b/client.go
index 8747949..d2bc343 100644
--- a/client.go
+++ b/client.go
@@ -96,6 +96,15 @@ func (r *Response) MetadataAsOperation() (*shared.Operation, error) {
 	return &op, nil
 }
 
+func (r *Response) MetadataAsStringSlice() ([]string, error) {
+	sl := []string{}
+	if err := json.Unmarshal(r.Metadata, &sl); err != nil {
+		return nil, err
+	}
+
+	return sl, nil
+}
+
 // ParseResponse parses a lxd style response out of an http.Response. Note that
 // this does _not_ automatically convert error responses to golang errors. To
 // do that, use ParseError. Internal client library uses should probably use
@@ -1771,6 +1780,7 @@ func (c *Client) PushFile(container string, p string, gid int, uid int, mode str
 		return err
 	}
 	req.Header.Set("User-Agent", shared.UserAgent)
+	req.Header.Set("X-LXD-Type", "file")
 
 	if mode != "" {
 		req.Header.Set("X-LXD-mode", mode)
@@ -1791,9 +1801,68 @@ func (c *Client) PushFile(container string, p string, gid int, uid int, mode str
 	return err
 }
 
-func (c *Client) PullFile(container string, p string) (int, int, int, io.ReadCloser, error) {
+func (c *Client) Mkdir(container string, p string, mode os.FileMode) error {
+	if c.Remote.Public {
+		return fmt.Errorf("This function isn't supported by public remotes.")
+	}
+
+	query := url.Values{"path": []string{p}}
+	uri := c.url(shared.APIVersion, "containers", container, "files") + "?" + query.Encode()
+
+	req, err := http.NewRequest("POST", uri, nil)
+	if err != nil {
+		return err
+	}
+
+	req.Header.Set("User-Agent", shared.UserAgent)
+	req.Header.Set("X-LXD-type", "directory")
+	req.Header.Set("X-LXD-mode", fmt.Sprintf("%04o", mode.Perm()))
+
+	raw, err := c.Http.Do(req)
+	if err != nil {
+		return err
+	}
+
+	_, err = HoistResponse(raw, Sync)
+	return err
+}
+
+func (c *Client) RecursivePushFile(container string, source string, target string) error {
+	if c.Remote.Public {
+		return fmt.Errorf("This function isn't supported by public remotes.")
+	}
+
+	sourceDir := filepath.Dir(source)
+
+	sendFile := func(p string, fInfo os.FileInfo, err error) error {
+		if err != nil {
+			return fmt.Errorf("got error sending path %s: %s", p, err)
+		}
+
+		targetPath := path.Join(target, p[len(sourceDir):])
+		if fInfo.IsDir() {
+			return c.Mkdir(container, targetPath, fInfo.Mode())
+		}
+
+		f, err := os.Open(p)
+		if err != nil {
+			return err
+		}
+		defer f.Close()
+
+		mode := fInfo.Mode()
+		uid := int(fInfo.Sys().(*syscall.Stat_t).Uid)
+		gid := int(fInfo.Sys().(*syscall.Stat_t).Gid)
+
+		return c.PushFile(container, targetPath, gid, uid, fmt.Sprintf("0%o", mode), f)
+	}
+
+	return filepath.Walk(source, sendFile)
+}
+
+func (c *Client) PullFile(container string, p string) (int, int, int, string, io.ReadCloser, []string, error) {
 	if c.Remote.Public {
-		return 0, 0, 0, nil, fmt.Errorf("This function isn't supported by public remotes.")
+		return 0, 0, 0, "", nil, nil, fmt.Errorf("This function isn't supported by public remotes.")
 	}
 
 	uri := c.url(shared.APIVersion, "containers", container, "files")
@@ -1801,12 +1870,73 @@ func (c *Client) PullFile(container string, p string) (int, int, int, io.ReadClo
 
 	r, err := c.getRaw(uri + "?" + query.Encode())
 	if err != nil {
-		return 0, 0, 0, nil, err
+		return 0, 0, 0, "", nil, nil, err
+	}
+
+	uid, gid, mode, type_ := shared.ParseLXDFileHeaders(r.Header)
+	if type_ == "directory" {
+		resp, err := HoistResponse(r, Sync)
+		if err != nil {
+			return 0, 0, 0, "", nil, nil, err
+		}
+
+		entries, err := resp.MetadataAsStringSlice()
+		if err != nil {
+			return 0, 0, 0, "", nil, nil, err
+		}
+
+		return uid, gid, mode, type_, nil, entries, nil
+	} else if type_ == "file" {
+		return uid, gid, mode, type_, r.Body, nil, nil
+	} else {
+		return 0, 0, 0, "", nil, nil, fmt.Errorf("unknown file type '%s'", type_)
+	}
+}
+
+func (c *Client) RecursivePullFile(container string, p string, targetDir string) error {
+	if c.Remote.Public {
+		return fmt.Errorf("This function isn't supported by public remotes.")
+	}
+
+	_, _, mode, type_, buf, entries, err := c.PullFile(container, p)
+	if err != nil {
+		return err
 	}
 
-	uid, gid, mode := shared.ParseLXDFileHeaders(r.Header)
+	target := filepath.Join(targetDir, filepath.Base(p))
 
-	return uid, gid, mode, r.Body, nil
+	if type_ == "directory" {
+		if err := os.Mkdir(target, os.FileMode(mode)); err != nil {
+			return err
+		}
+
+		for _, ent := range entries {
+			nextP := path.Join(p, ent)
+			if err := c.RecursivePullFile(container, nextP, target); err != nil {
+				return err
+			}
+		}
+	} else if type_ == "file" {
+		f, err := os.Create(target)
+		if err != nil {
+			return err
+		}
+		defer f.Close()
+
+		err = f.Chmod(os.FileMode(mode))
+		if err != nil {
+			return err
+		}
+
+		_, err = io.Copy(f, buf)
+		if err != nil {
+			return err
+		}
+	} else {
+		return fmt.Errorf("unknown file type '%s'", type_)
+	}
+
+	return nil
 }
 
 func (c *Client) GetMigrationSourceWS(container string) (*Response, error) {
diff --git a/lxc/file.go b/lxc/file.go
index 40d5994..c8b56a8 100644
--- a/lxc/file.go
+++ b/lxc/file.go
@@ -22,6 +22,8 @@ type fileCmd struct {
 	uid  int
 	gid  int
 	mode string
+
+	recursive bool
 }
 
 func (c *fileCmd) showByDefault() bool {
@@ -32,17 +34,28 @@ func (c *fileCmd) usage() string {
 	return i18n.G(
 		`Manage files on a container.
 
-lxc file pull <source> [<source>...] <target>
-lxc file push [--uid=UID] [--gid=GID] [--mode=MODE] <source> [<source>...] <target>
+lxc file pull [-r|--recursive] <source> [<source>...] <target>
+lxc file push [-r|--recursive] [--uid=UID] [--gid=GID] [--mode=MODE] <source> [<source>...] <target>
 lxc file edit <file>
 
-<source> in the case of pull, <target> in the case of push and <file> in the case of edit are <container name>/<path>`)
+<source> in the case of pull, <target> in the case of push and <file> in the case of edit are <container name>/<path>
+
+Examples:
+
+To push /etc/hosts into the container foo:
+  lxc file push /etc/hosts foo/etc/hosts
+
+To pull /etc/hosts from the container:
+  lxc file pull foo/etc/hosts .
+`)
 }
 
 func (c *fileCmd) flags() {
 	gnuflag.IntVar(&c.uid, "uid", -1, i18n.G("Set the file's uid on push"))
 	gnuflag.IntVar(&c.gid, "gid", -1, i18n.G("Set the file's gid on push"))
 	gnuflag.StringVar(&c.mode, "mode", "", i18n.G("Set the file's perms on push"))
+	gnuflag.BoolVar(&c.recursive, "recusrive", false, i18n.G("Recursively push or pull files"))
+	gnuflag.BoolVar(&c.recursive, "r", false, i18n.G("Recursively push or pull files"))
 }
 
 func (c *fileCmd) push(config *lxd.Config, send_file_perms bool, args []string) error {
@@ -65,6 +78,27 @@ func (c *fileCmd) push(config *lxd.Config, send_file_perms bool, args []string)
 		return err
 	}
 
+	var sourcefilenames []string
+	for _, fname := range args[:len(args)-1] {
+		if !strings.HasPrefix(fname, "--") {
+			sourcefilenames = append(sourcefilenames, fname)
+		}
+	}
+
+	if c.recursive {
+		if c.uid != -1 || c.gid != -1 || c.mode != "" {
+			return fmt.Errorf(i18n.G("can't supply uid/gid/mode in recursive mode"))
+		}
+
+		for _, fname := range sourcefilenames {
+			if err := d.RecursivePushFile(container, fname, pathSpec[1]); err != nil {
+				return err
+			}
+		}
+
+		return nil
+	}
+
 	mode := os.FileMode(0755)
 	if c.mode != "" {
 		if len(c.mode) == 3 {
@@ -90,13 +124,6 @@ func (c *fileCmd) push(config *lxd.Config, send_file_perms bool, args []string)
 
 	_, targetfilename := filepath.Split(targetPath)
 
-	var sourcefilenames []string
-	for _, fname := range args[:len(args)-1] {
-		if !strings.HasPrefix(fname, "--") {
-			sourcefilenames = append(sourcefilenames, fname)
-		}
-	}
-
 	if (targetfilename != "") && (len(sourcefilenames) > 1) {
 		return errArgs
 	}
@@ -201,11 +228,23 @@ func (c *fileCmd) pull(config *lxd.Config, args []string) error {
 			return err
 		}
 
-		_, _, _, buf, err := d.PullFile(container, pathSpec[1])
+		if c.recursive {
+			if err := d.RecursivePullFile(container, pathSpec[1], target); err != nil {
+				return err
+			}
+
+			continue
+		}
+
+		_, _, mode, type_, buf, _, err := d.PullFile(container, pathSpec[1])
 		if err != nil {
 			return err
 		}
 
+		if type_ == "directory" {
+			return fmt.Errorf(i18n.G("can't pull a directory without --recursive"))
+		}
+
 		var targetPath string
 		if targetIsDir {
 			targetPath = path.Join(target, path.Base(pathSpec[1]))
@@ -222,6 +261,11 @@ func (c *fileCmd) pull(config *lxd.Config, args []string) error {
 				return err
 			}
 			defer f.Close()
+
+			err = f.Chmod(os.FileMode(mode))
+			if err != nil {
+				return err
+			}
 		}
 
 		_, err = io.Copy(f, buf)
@@ -238,6 +282,10 @@ func (c *fileCmd) edit(config *lxd.Config, args []string) error {
 		return errArgs
 	}
 
+	if c.recursive {
+		return fmt.Errorf(i18n.G("recursive edit doesn't make sense :("))
+	}
+
 	// If stdin isn't a terminal, read text from it
 	if !termios.IsTerminal(int(syscall.Stdin)) {
 		return c.push(config, false, append([]string{os.Stdin.Name()}, args[0]))
diff --git a/lxd/container.go b/lxd/container.go
index e2a0980..87c78dd 100644
--- a/lxd/container.go
+++ b/lxd/container.go
@@ -319,7 +319,7 @@ type container interface {
 	ConfigKeySet(key string, value string) error
 
 	// File handling
-	FilePull(srcpath string, dstpath string) (int, int, os.FileMode, error)
+	FilePull(srcpath string, dstpath string) (int, int, os.FileMode, string, []string, error)
 	FilePush(srcpath string, dstpath string, uid int, gid int, mode int) error
 
 	// Command execution
diff --git a/lxd/container_file.go b/lxd/container_file.go
index 7ac2085..e682061 100644
--- a/lxd/container_file.go
+++ b/lxd/container_file.go
@@ -49,8 +49,8 @@ func containerFileGet(c container, path string, r *http.Request) Response {
 	}
 	defer temp.Close()
 
-	// Pul the file from the container
-	uid, gid, mode, err := c.FilePull(path, temp.Name())
+	// Pull the file from the container
+	uid, gid, mode, type_, dirEnts, err := c.FilePull(path, temp.Name())
 	if err != nil {
 		return SmartError(err)
 	}
@@ -59,41 +59,58 @@ func containerFileGet(c container, path string, r *http.Request) Response {
 		"X-LXD-uid":  fmt.Sprintf("%d", uid),
 		"X-LXD-gid":  fmt.Sprintf("%d", gid),
 		"X-LXD-mode": fmt.Sprintf("%04o", mode),
+		"X-LXD-type": type_,
 	}
 
-	// Make a file response struct
-	files := make([]fileResponseEntry, 1)
-	files[0].identifier = filepath.Base(path)
-	files[0].path = temp.Name()
-	files[0].filename = filepath.Base(path)
-
-	return FileResponse(r, files, headers, true)
+	if type_ == "file" {
+		// Make a file response struct
+		files := make([]fileResponseEntry, 1)
+		files[0].identifier = filepath.Base(path)
+		files[0].path = temp.Name()
+		files[0].filename = filepath.Base(path)
+
+		return FileResponse(r, files, headers, true)
+	} else if type_ == "directory" {
+		return SyncResponseHeaders(true, dirEnts, headers)
+	} else {
+		return InternalError(fmt.Errorf("bad file type %s", type_))
+	}
 }
 
 func containerFilePut(c container, path string, r *http.Request) Response {
 	// Extract file ownership and mode from headers
-	uid, gid, mode := shared.ParseLXDFileHeaders(r.Header)
-
-	// Write file content to a tempfile
-	temp, err := ioutil.TempFile("", "lxd_forkputfile_")
-	if err != nil {
-		return InternalError(err)
-	}
-	defer func() {
-		temp.Close()
-		os.Remove(temp.Name())
-	}()
-
-	_, err = io.Copy(temp, r.Body)
-	if err != nil {
-		return InternalError(err)
+	uid, gid, mode, type_ := shared.ParseLXDFileHeaders(r.Header)
+
+	if type_ == "file" {
+		// Write file content to a tempfile
+		temp, err := ioutil.TempFile("", "lxd_forkputfile_")
+		if err != nil {
+			return InternalError(err)
+		}
+		defer func() {
+			temp.Close()
+			os.Remove(temp.Name())
+		}()
+
+		_, err = io.Copy(temp, r.Body)
+		if err != nil {
+			return InternalError(err)
+		}
+
+		// Transfer the file into the container
+		err = c.FilePush(temp.Name(), path, uid, gid, mode)
+		if err != nil {
+			return InternalError(err)
+		}
+
+		return EmptySyncResponse
+	} else if type_ == "directory" {
+		err := c.FilePush("", path, uid, gid, mode)
+		if err != nil {
+			return InternalError(err)
+		}
+		return EmptySyncResponse
+	} else {
+		return InternalError(fmt.Errorf("bad file type %s", type_))
 	}
-
-	// Transfer the file into the container
-	err = c.FilePush(temp.Name(), path, uid, gid, mode)
-	if err != nil {
-		return InternalError(err)
-	}
-
-	return EmptySyncResponse
 }
diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go
index aeac665..e12dbc8 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -3163,12 +3163,12 @@ func (c *containerLXC) templateApplyNow(trigger string) error {
 	return nil
 }
 
-func (c *containerLXC) FilePull(srcpath string, dstpath string) (int, int, os.FileMode, error) {
+func (c *containerLXC) FilePull(srcpath string, dstpath string) (int, int, os.FileMode, string, []string, error) {
 	// Setup container storage if needed
 	if !c.IsRunning() {
 		err := c.StorageStart()
 		if err != nil {
-			return -1, -1, 0, err
+			return -1, -1, 0, "", nil, err
 		}
 	}
 
@@ -3186,13 +3186,15 @@ func (c *containerLXC) FilePull(srcpath string, dstpath string) (int, int, os.Fi
 	if !c.IsRunning() {
 		err := c.StorageStop()
 		if err != nil {
-			return -1, -1, 0, err
+			return -1, -1, 0, "", nil, err
 		}
 	}
 
 	uid := -1
 	gid := -1
 	mode := -1
+	type_ := "unknown"
+	var dirEnts []string
 
 	var errStr string
 
@@ -3211,16 +3213,16 @@ func (c *containerLXC) FilePull(srcpath string, dstpath string) (int, int, os.Fi
 		if strings.HasPrefix(line, "errno: ") {
 			errno := strings.TrimPrefix(line, "errno: ")
 			if errno == "2" {
-				return -1, -1, 0, os.ErrNotExist
+				return -1, -1, 0, "", nil, os.ErrNotExist
 			}
-			return -1, -1, 0, fmt.Errorf(errStr)
+			return -1, -1, 0, "", nil, fmt.Errorf(errStr)
 		}
 
 		// Extract the uid
 		if strings.HasPrefix(line, "uid: ") {
 			uid, err = strconv.Atoi(strings.TrimPrefix(line, "uid: "))
 			if err != nil {
-				return -1, -1, 0, err
+				return -1, -1, 0, "", nil, err
 			}
 
 			continue
@@ -3230,7 +3232,7 @@ func (c *containerLXC) FilePull(srcpath string, dstpath string) (int, int, os.Fi
 		if strings.HasPrefix(line, "gid: ") {
 			gid, err = strconv.Atoi(strings.TrimPrefix(line, "gid: "))
 			if err != nil {
-				return -1, -1, 0, err
+				return -1, -1, 0, "", nil, err
 			}
 
 			continue
@@ -3240,17 +3242,29 @@ func (c *containerLXC) FilePull(srcpath string, dstpath string) (int, int, os.Fi
 		if strings.HasPrefix(line, "mode: ") {
 			mode, err = strconv.Atoi(strings.TrimPrefix(line, "mode: "))
 			if err != nil {
-				return -1, -1, 0, err
+				return -1, -1, 0, "", nil, err
 			}
 
 			continue
 		}
 
+		if strings.HasPrefix(line, "type: ") {
+			type_ = strings.TrimPrefix(line, "type: ")
+			continue
+		}
+
+		if strings.HasPrefix(line, "entry: ") {
+			ent := strings.TrimPrefix(line, "entry: ")
+			ent = strings.Replace(ent, "\x00", "\n", -1)
+			dirEnts = append(dirEnts, ent)
+			continue
+		}
+
 		shared.Debugf("forkgetfile: %s", line)
 	}
 
 	if err != nil {
-		return -1, -1, 0, fmt.Errorf(
+		return -1, -1, 0, "", nil, fmt.Errorf(
 			"Error calling 'lxd forkgetfile %s %d %s': err='%v'",
 			dstpath,
 			c.InitPID(),
@@ -3261,14 +3275,14 @@ func (c *containerLXC) FilePull(srcpath string, dstpath string) (int, int, os.Fi
 	// Unmap uid and gid if needed
 	idmapset, err := c.LastIdmapSet()
 	if err != nil {
-		return -1, -1, 0, err
+		return -1, -1, 0, "", nil, err
 	}
 
 	if idmapset != nil {
 		uid, gid = idmapset.ShiftFromNs(uid, gid)
 	}
 
-	return uid, gid, os.FileMode(mode), nil
+	return uid, gid, os.FileMode(mode), type_, dirEnts, nil
 }
 
 func (c *containerLXC) FilePush(srcpath string, dstpath string, uid int, gid int, mode int) error {
diff --git a/lxd/nsexec.go b/lxd/nsexec.go
index 30e2d1a..edd0bbf 100644
--- a/lxd/nsexec.go
+++ b/lxd/nsexec.go
@@ -37,6 +37,7 @@ package main
 #include <alloca.h>
 #include <libgen.h>
 #include <ifaddrs.h>
+#include <dirent.h>
 
 // This expects:
 //  ./lxd forkputfile /source/path <pid> /target/path
@@ -126,22 +127,21 @@ int dosetns(int pid, char *nstype) {
 }
 
 int manip_file_in_ns(char *rootfs, int pid, char *host, char *container, bool is_put, uid_t uid, gid_t gid, mode_t mode, uid_t defaultUid, gid_t defaultGid, mode_t defaultMode) {
-	int host_fd, container_fd;
+	int host_fd = -1, container_fd = -1;
 	int ret = -1;
 	int container_open_flags;
 	struct stat st;
 	int exists = 1;
+	bool is_dir_manip = !strcmp(host, "");
 
-	host_fd = open(host, O_RDWR);
-	if (host_fd < 0) {
-		error("error: open");
-		return -1;
+	if (!is_dir_manip) {
+		host_fd = open(host, O_RDWR);
+		if (host_fd < 0) {
+			error("error: open");
+			return -1;
+		}
 	}
 
-	container_open_flags = O_RDWR;
-	if (is_put)
-		container_open_flags |= O_CREAT;
-
 	if (pid > 0) {
 		if (dosetns(pid, "mnt") < 0) {
 			error("error: setns");
@@ -159,9 +159,25 @@ int manip_file_in_ns(char *rootfs, int pid, char *host, char *container, bool is
 		}
 	}
 
-	if (is_put && stat(container, &st) < 0)
+	if (is_put && is_dir_manip) {
+		if (mkdir(container, mode) < 0 && errno != EEXIST) {
+			error("error: mkdir");
+			return -1;
+		}
+
+		return 0;
+	}
+
+	if (stat(container, &st) < 0)
 		exists = 0;
 
+	container_open_flags = O_RDWR;
+	if (is_put)
+		container_open_flags |= O_CREAT;
+
+	if (exists && S_ISDIR(st.st_mode))
+		container_open_flags = O_DIRECTORY;
+
 	umask(0);
 	container_fd = open(container, container_open_flags, 0);
 	if (container_fd < 0) {
@@ -198,10 +214,8 @@ int manip_file_in_ns(char *rootfs, int pid, char *host, char *container, bool is
 			error("error: chown");
 			goto close_container;
 		}
-
 		ret = 0;
 	} else {
-		ret = copy(host_fd, container_fd);
 
 		if (fstat(container_fd, &st) < 0) {
 			error("error: stat");
@@ -211,6 +225,43 @@ int manip_file_in_ns(char *rootfs, int pid, char *host, char *container, bool is
 		fprintf(stderr, "uid: %ld\n", (long)st.st_uid);
 		fprintf(stderr, "gid: %ld\n", (long)st.st_gid);
 		fprintf(stderr, "mode: %ld\n", (unsigned long)st.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO));
+		if (S_ISDIR(st.st_mode)) {
+			DIR *fdir;
+			struct dirent *de;
+
+			fdir = fdopendir(container_fd);
+			if (!fdir) {
+				error("error: fdopendir");
+				goto close_container;
+			}
+
+			fprintf(stderr, "type: directory\n");
+
+			while((de = readdir(fdir))) {
+				int len, i;
+
+				if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, ".."))
+					continue;
+
+				fprintf(stderr, "entry: ");
+
+				// swap \n to \0 since we split this output by line
+				for (i = 0, len = strlen(de->d_name); i < len; i++) {
+					if (*(de->d_name + i) == '\n')
+						putc(0, stderr);
+					else
+						putc(*(de->d_name + i), stderr);
+				}
+				fprintf(stderr, "\n");
+			}
+
+			// container_fd is dead now that we fopendir'd it
+			goto close_host;
+		} else {
+			fprintf(stderr, "type: file\n");
+			ret = copy(host_fd, container_fd);
+		}
+		fprintf(stderr, "type: %s", S_ISDIR(st.st_mode) ? "directory" : "file");
 	}
 
 close_container:
diff --git a/lxd/response.go b/lxd/response.go
index fc37f9c..1116857 100644
--- a/lxd/response.go
+++ b/lxd/response.go
@@ -42,6 +42,7 @@ type syncResponse struct {
 	etag     interface{}
 	metadata interface{}
 	location string
+	headers  map[string]string
 }
 
 func (r *syncResponse) Render(w http.ResponseWriter) error {
@@ -59,6 +60,12 @@ func (r *syncResponse) Render(w http.ResponseWriter) error {
 		status = shared.Failure
 	}
 
+	if r.headers != nil {
+		for h, v := range r.headers {
+			w.Header().Set(h, v)
+		}
+	}
+
 	if r.location != "" {
 		w.Header().Set("Location", r.location)
 		w.WriteHeader(201)
@@ -88,6 +95,10 @@ func SyncResponseLocation(success bool, metadata interface{}, location string) R
 	return &syncResponse{success: success, metadata: metadata, location: location}
 }
 
+func SyncResponseHeaders(success bool, metadata interface{}, headers map[string]string) Response {
+	return &syncResponse{success: success, metadata: metadata, headers: headers}
+}
+
 var EmptySyncResponse = &syncResponse{success: true, metadata: make(map[string]interface{})}
 
 // File transfer response
diff --git a/shared/util.go b/shared/util.go
index b6539c2..a40874f 100644
--- a/shared/util.go
+++ b/shared/util.go
@@ -106,7 +106,7 @@ func LogPath(path ...string) string {
 	return filepath.Join(items...)
 }
 
-func ParseLXDFileHeaders(headers http.Header) (uid int, gid int, mode int) {
+func ParseLXDFileHeaders(headers http.Header) (uid int, gid int, mode int, type_ string) {
 	uid, err := strconv.Atoi(headers.Get("X-LXD-uid"))
 	if err != nil {
 		uid = -1
@@ -127,7 +127,15 @@ func ParseLXDFileHeaders(headers http.Header) (uid int, gid int, mode int) {
 		}
 	}
 
-	return uid, gid, mode
+	type_ = headers.Get("X-LXD-type")
+	/* backwards compat: before "type" was introduced, we could only
+	 * manipulate files
+	 */
+	if type_ == "" {
+		type_ = "file"
+	}
+
+	return uid, gid, mode, type_
 }
 
 func ReadToJSON(r io.Reader, req interface{}) error {
diff --git a/test/suites/filemanip.sh b/test/suites/filemanip.sh
index 07ca148..2282edc 100644
--- a/test/suites/filemanip.sh
+++ b/test/suites/filemanip.sh
@@ -15,5 +15,17 @@ test_filemanip() {
   err=$(my_curl -o /dev/null -w "%{http_code}" -X GET "https://${LXD_ADDR}/1.0/containers/filemanip/files?path=/tmp/foo")
   [ "${err}" -eq "404" ]
 
+  # lxc {push|pull} -r
+  mkdir "${TEST_DIR}"/source
+  echo "foo" > "${TEST_DIR}"/source/foo
+  echo "bar" > "${TEST_DIR}"/source/bar
+
+  lxc file push -r "${TEST_DIR}"/source filemanip/tmp
+  mkdir "${TEST_DIR}"/dest
+  lxc file pull -r filemanip/tmp/source "${TEST_DIR}"/dest
+
+  [ "$(cat "${TEST_DIR}"/dest/source/foo)" = "foo" ]
+  [ "$(cat "${TEST_DIR}"/dest/source/bar)" = "bar" ]
+
   lxc delete filemanip -f
 }

From 510b038ab80b67e4e930327f72d216028361de56 Mon Sep 17 00:00:00 2001
From: Tycho Andersen <tycho.andersen at canonical.com>
Date: Wed, 7 Sep 2016 22:46:29 +0000
Subject: [PATCH 2/2] implement `lxc file push -p` to enable creating
 directories

Closes #2290

Signed-off-by: Tycho Andersen <tycho.andersen at canonical.com>
---
 client.go                | 33 +++++++++++++++++++++++++++++++++
 lxc/file.go              | 44 ++++++++++++++++++++++++++++++--------------
 test/suites/filemanip.sh |  5 +++++
 3 files changed, 68 insertions(+), 14 deletions(-)

diff --git a/client.go b/client.go
index d2bc343..c869fbb 100644
--- a/client.go
+++ b/client.go
@@ -1827,6 +1827,39 @@ func (c *Client) Mkdir(container string, p string, mode os.FileMode) error {
 	return err
 }
 
+func (c *Client) MkdirP(container string, p string, mode os.FileMode) error {
+	if c.Remote.Public {
+		return fmt.Errorf("This function isn't supported by public remotes.")
+	}
+
+	parts := strings.Split(p, "/")
+	i := len(parts)
+
+	for ; i >= 1; i-- {
+		cur := filepath.Join(parts[:i]...)
+		_, _, _, type_, _, _, err := c.PullFile(container, cur)
+		if err != nil {
+			continue
+		}
+
+		if type_ != "directory" {
+			return fmt.Errorf("%s is not a directory", cur)
+		}
+
+		i++
+		break
+	}
+
+	for ; i <= len(parts); i++ {
+		cur := filepath.Join(parts[:i]...)
+		if err := c.Mkdir(container, cur, mode); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
 func (c *Client) RecursivePushFile(container string, source string, target string) error {
 	if c.Remote.Public {
 		return fmt.Errorf("This function isn't supported by public remotes.")
diff --git a/lxc/file.go b/lxc/file.go
index c8b56a8..f60c719 100644
--- a/lxc/file.go
+++ b/lxc/file.go
@@ -24,6 +24,8 @@ type fileCmd struct {
 	mode string
 
 	recursive bool
+
+	mkdirs bool
 }
 
 func (c *fileCmd) showByDefault() bool {
@@ -35,7 +37,7 @@ func (c *fileCmd) usage() string {
 		`Manage files on a container.
 
 lxc file pull [-r|--recursive] <source> [<source>...] <target>
-lxc file push [-r|--recursive] [--uid=UID] [--gid=GID] [--mode=MODE] <source> [<source>...] <target>
+lxc file push [-r|--recursive] [-p|create-dirs] [--uid=UID] [--gid=GID] [--mode=MODE] <source> [<source>...] <target>
 lxc file edit <file>
 
 <source> in the case of pull, <target> in the case of push and <file> in the case of edit are <container name>/<path>
@@ -56,6 +58,8 @@ func (c *fileCmd) flags() {
 	gnuflag.StringVar(&c.mode, "mode", "", i18n.G("Set the file's perms on push"))
 	gnuflag.BoolVar(&c.recursive, "recusrive", false, i18n.G("Recursively push or pull files"))
 	gnuflag.BoolVar(&c.recursive, "r", false, i18n.G("Recursively push or pull files"))
+	gnuflag.BoolVar(&c.mkdirs, "create-dirs", false, i18n.G("Create any directories necessary"))
+	gnuflag.BoolVar(&c.mkdirs, "p", false, i18n.G("Create any directories necessary"))
 }
 
 func (c *fileCmd) push(config *lxd.Config, send_file_perms bool, args []string) error {
@@ -85,12 +89,31 @@ func (c *fileCmd) push(config *lxd.Config, send_file_perms bool, args []string)
 		}
 	}
 
+	mode := os.FileMode(0755)
+	if c.mode != "" {
+		if len(c.mode) == 3 {
+			c.mode = "0" + c.mode
+		}
+
+		m, err := strconv.ParseInt(c.mode, 0, 0)
+		if err != nil {
+			return err
+		}
+		mode = os.FileMode(m)
+	}
+
 	if c.recursive {
 		if c.uid != -1 || c.gid != -1 || c.mode != "" {
 			return fmt.Errorf(i18n.G("can't supply uid/gid/mode in recursive mode"))
 		}
 
 		for _, fname := range sourcefilenames {
+			if c.mkdirs {
+				if err := d.MkdirP(container, fname, mode); err != nil {
+					return err
+				}
+			}
+
 			if err := d.RecursivePushFile(container, fname, pathSpec[1]); err != nil {
 				return err
 			}
@@ -99,19 +122,6 @@ func (c *fileCmd) push(config *lxd.Config, send_file_perms bool, args []string)
 		return nil
 	}
 
-	mode := os.FileMode(0755)
-	if c.mode != "" {
-		if len(c.mode) == 3 {
-			c.mode = "0" + c.mode
-		}
-
-		m, err := strconv.ParseInt(c.mode, 0, 0)
-		if err != nil {
-			return err
-		}
-		mode = os.FileMode(m)
-	}
-
 	uid := 0
 	if c.uid >= 0 {
 		uid = c.uid
@@ -152,6 +162,12 @@ func (c *fileCmd) push(config *lxd.Config, send_file_perms bool, args []string)
 			fpath = path.Join(fpath, path.Base(f.Name()))
 		}
 
+		if c.mkdirs {
+			if err := d.MkdirP(container, filepath.Dir(fpath), mode); err != nil {
+				return err
+			}
+		}
+
 		if send_file_perms {
 			if c.mode == "" || c.uid == -1 || c.gid == -1 {
 				fMode, fUid, fGid, err := c.getOwner(f)
diff --git a/test/suites/filemanip.sh b/test/suites/filemanip.sh
index 2282edc..6fb51d1 100644
--- a/test/suites/filemanip.sh
+++ b/test/suites/filemanip.sh
@@ -27,5 +27,10 @@ test_filemanip() {
   [ "$(cat "${TEST_DIR}"/dest/source/foo)" = "foo" ]
   [ "$(cat "${TEST_DIR}"/dest/source/bar)" = "bar" ]
 
+  lxc file push -p "${TEST_DIR}"/source/foo filemanip/tmp/this/is/a/nonexistent/directory/
+  echo done with push
+  lxc file pull filemanip/tmp/this/is/a/nonexistent/directory/foo "${TEST_DIR}"
+  [ "$(cat "${TEST_DIR}"/foo)" = "foo" ]
+
   lxc delete filemanip -f
 }


More information about the lxc-devel mailing list