Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add incus file create subcommand #408

Merged
merged 4 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 198 additions & 7 deletions cmd/incus/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@ import (
"github.com/lxc/incus/shared/util"
)

// DirMode represents the file mode for creating dirs on `incus file pull/push`.
const DirMode = 0755
const (
// DirMode represents the file mode for creating dirs on `incus file pull/push`.
DirMode = 0755
// FileMode represents the file mode for creating files on `incus file create`.
FileMode = 0644
)

type cmdFile struct {
global *cmdGlobal
Expand Down Expand Up @@ -80,10 +84,18 @@ func (c *cmdFile) Command() *cobra.Command {
cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G(
`Manage files in instances`))

// Create
fileCreateCmd := cmdFileCreate{global: c.global, file: c}
cmd.AddCommand(fileCreateCmd.Command())

// Delete
fileDeleteCmd := cmdFileDelete{global: c.global, file: c}
cmd.AddCommand(fileDeleteCmd.Command())

// Mount
fileMountCmd := cmdFileMount{global: c.global, file: c}
cmd.AddCommand(fileMountCmd.Command())

// Pull
filePullCmd := cmdFilePull{global: c.global, file: c}
cmd.AddCommand(filePullCmd.Command())
Expand All @@ -96,16 +108,194 @@ func (c *cmdFile) Command() *cobra.Command {
fileEditCmd := cmdFileEdit{global: c.global, file: c, filePull: &filePullCmd, filePush: &filePushCmd}
cmd.AddCommand(fileEditCmd.Command())

// Mount
fileMountCmd := cmdFileMount{global: c.global, file: c}
cmd.AddCommand(fileMountCmd.Command())

// Workaround for subcommand usage errors. See: https://github.com/spf13/cobra/issues/706
cmd.Args = cobra.NoArgs
cmd.Run = func(cmd *cobra.Command, args []string) { _ = cmd.Usage() }
return cmd
}

// Create.
type cmdFileCreate struct {
global *cmdGlobal
file *cmdFile

flagContent string
flagForce bool
flagType string
}

// Command returns the cobra command for `file create`.
func (c *cmdFileCreate) Command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = usage("create", i18n.G("[<remote>:]<instance>/<path> [<symlink target path>]"))
cmd.Short = i18n.G("Create files and directories in instances")
cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G(
`Create files and directories in instances`))
cmd.Example = cli.FormatSection("", i18n.G(
`incus file create foo/bar
To create a file /bar in the foo instance.
incus file create --type=symlink foo/bar baz
To create a symlink /bar in instance foo whose target is baz.`))

cmd.Flags().BoolVarP(&c.file.flagMkdir, "create-dirs", "p", false, i18n.G("Create any directories necessary")+"``")
cmd.Flags().BoolVarP(&c.flagForce, "force", "f", false, i18n.G("Force creating files or directories")+"``")
cmd.Flags().IntVar(&c.file.flagGID, "gid", -1, i18n.G("Set the file's gid on create")+"``")
cmd.Flags().IntVar(&c.file.flagUID, "uid", -1, i18n.G("Set the file's uid on create")+"``")
cmd.Flags().StringVar(&c.file.flagMode, "mode", "", i18n.G("Set the file's perms on create")+"``")
cmd.Flags().StringVar(&c.flagType, "type", "file", i18n.G("The type to create (file, symlink, or directory)")+"``")
cmd.RunE = c.Run

return cmd
}

// Run runs the `file create` command.
func (c *cmdFileCreate) Run(cmd *cobra.Command, args []string) error {
// Quick checks.
exit, err := c.global.CheckArgs(cmd, args, 1, 2)
if exit {
return err
}

if !util.ValueInSlice(c.flagType, []string{"file", "symlink", "directory"}) {
return fmt.Errorf(i18n.G("Invalid type %q"), c.flagType)
}

if len(args) == 2 && c.flagType != "symlink" {
return fmt.Errorf(i18n.G(`Symlink target path can only be used for type "symlink"`))
}

if strings.HasSuffix(args[0], "/") {
c.flagType = "directory"
}

pathSpec := strings.SplitN(args[0], "/", 2)

if len(pathSpec) != 2 {
return fmt.Errorf(i18n.G("Invalid target %s"), args[0])
}

// Parse remote.
resources, err := c.global.ParseServers(pathSpec[0])
if err != nil {
return err
}

resource := resources[0]

// re-add leading / that got stripped by the SplitN
targetPath := path.Clean("/" + pathSpec[1])

// normalization may reveal that path is still a dir, e.g. /.
if strings.HasSuffix(targetPath, "/") {
c.flagType = "directory"
}

var symlinkTargetPath string

// Determine the target if specified.
if len(args) == 2 {
symlinkTargetPath = filepath.Clean(args[1])
}

// Determine the target uid
uid := 0
if c.file.flagUID > 0 {
uid = c.file.flagUID
}

// Determine the target gid
gid := 0
if c.file.flagGID > 0 {
gid = c.file.flagGID
}

var mode os.FileMode

// Determine the target mode
if c.flagType == "directory" {
mode = os.FileMode(DirMode)
} else if c.flagType == "file" {
mode = os.FileMode(FileMode)
}

if c.file.flagMode != "" {
if len(c.file.flagMode) == 3 {
c.file.flagMode = "0" + c.file.flagMode
}

m, err := strconv.ParseInt(c.file.flagMode, 0, 0)
if err != nil {
return err
}

mode = os.FileMode(m)
}

// Create needed paths if requested
if c.file.flagMkdir {
err = c.file.recursiveMkdir(resource.server, resource.name, path.Dir(targetPath), nil, int64(uid), int64(gid))
if err != nil {
return err
}
}

var content io.ReadSeeker
var readCloser io.ReadCloser
var contentLength int64

if c.flagType == "symlink" {
content = strings.NewReader(symlinkTargetPath)
readCloser = io.NopCloser(content)
contentLength = int64(len(symlinkTargetPath))
} else if c.flagType == "file" {
// Just creating an empty file.
content = strings.NewReader("")
readCloser = io.NopCloser(content)
contentLength = 0
}

fileArgs := incus.InstanceFileArgs{
Type: c.flagType,
UID: int64(uid),
GID: int64(gid),
Mode: int(mode.Perm()),
Content: content,
}

if c.flagForce {
fileArgs.WriteMode = "overwrite"
}

progress := cli.ProgressRenderer{
Format: fmt.Sprintf(i18n.G("Creating %s: %%s"), targetPath),
Quiet: c.global.flagQuiet,
}

if readCloser != nil {
fileArgs.Content = internalIO.NewReadSeeker(&ioprogress.ProgressReader{
ReadCloser: readCloser,
Tracker: &ioprogress.ProgressTracker{
Length: contentLength,
Handler: func(percent int64, speed int64) {
progress.UpdateProgress(ioprogress.ProgressData{
Text: fmt.Sprintf("%d%% (%s/s)", percent, units.GetByteSizeString(speed, 2)),
})
},
},
}, fileArgs.Content)
}

err = resource.server.CreateInstanceFile(resource.name, targetPath, fileArgs)
if err != nil {
progress.Done("")
return err
}

progress.Done("")

return nil
}

// Delete.
type cmdFileDelete struct {
global *cmdGlobal
Expand Down Expand Up @@ -612,8 +802,9 @@ func (c *cmdFilePush) Run(cmd *cobra.Command, args []string) error {
return err
}

_, dUID, dGID := internalIO.GetOwnerMode(finfo)
if c.file.flagUID == -1 || c.file.flagGID == -1 {
_, dUID, dGID := internalIO.GetOwnerMode(finfo)

if c.file.flagUID == -1 {
uid = dUID
}
Expand Down
Loading
Loading