Add support for file editor

This commit is contained in:
marcos
2018-01-05 13:13:07 -03:00
parent f564f1fd28
commit 386bd87385
32 changed files with 621 additions and 93 deletions

View File

@@ -32,27 +32,34 @@ const (
type DockerApi interface {
GetClient() *client.Client
CreateNetwork(id string, opts types.NetworkCreate) error
ConnectNetwork(container, network, ip string) (string, error)
NetworkCreate(id string, opts types.NetworkCreate) error
NetworkConnect(container, network, ip string) (string, error)
NetworkInspect(id string) (types.NetworkResource, error)
GetDaemonInfo() (types.Info, error)
GetDaemonHost() string
NetworkDelete(id string) error
NetworkDisconnect(containerId, networkId string) error
DaemonInfo() (types.Info, error)
DaemonHost() string
GetSwarmPorts() ([]string, []uint16, error)
GetPorts() ([]uint16, error)
GetContainerStats(name string) (io.ReadCloser, error)
ContainerStats(name string) (io.ReadCloser, error)
ContainerResize(name string, rows, cols uint) error
ContainerRename(old, new string) error
ContainerDelete(name string) error
ContainerCreate(opts CreateContainerOpts) error
ContainerIPs(id string) (map[string]string, error)
ExecAttach(instanceName string, command []string, out io.Writer) (int, error)
Exec(instanceName string, command []string) (int, error)
CreateAttachConnection(name string) (net.Conn, error)
CopyToContainer(containerName, destination, fileName string, content io.Reader) error
DeleteContainer(name string) error
CreateContainer(opts CreateContainerOpts) error
GetContainerIPs(id string) (map[string]string, error)
ExecAttach(instanceName string, command []string, out io.Writer) (int, error)
DisconnectNetwork(containerId, networkId string) error
DeleteNetwork(id string) error
Exec(instanceName string, command []string) (int, error)
CopyFromContainer(containerName, filePath string) (io.Reader, error)
SwarmInit(advertiseAddr string) (*SwarmTokens, error)
SwarmJoin(addr, token string) error
ConfigCreate(name string, labels map[string]string, data []byte) error
ConfigDelete(name string) error
}
@@ -82,7 +89,7 @@ func (d *docker) ConfigDelete(name string) error {
return d.c.ConfigRemove(context.Background(), name)
}
func (d *docker) CreateNetwork(id string, opts types.NetworkCreate) error {
func (d *docker) NetworkCreate(id string, opts types.NetworkCreate) error {
_, err := d.c.NetworkCreate(context.Background(), id, opts)
if err != nil {
@@ -94,7 +101,7 @@ func (d *docker) CreateNetwork(id string, opts types.NetworkCreate) error {
return nil
}
func (d *docker) ConnectNetwork(containerId, networkId, ip string) (string, error) {
func (d *docker) NetworkConnect(containerId, networkId, ip string) (string, error) {
settings := &network.EndpointSettings{}
if ip != "" {
settings.IPAddress = ip
@@ -125,11 +132,11 @@ func (d *docker) NetworkInspect(id string) (types.NetworkResource, error) {
return d.c.NetworkInspect(context.Background(), id, types.NetworkInspectOptions{})
}
func (d *docker) GetDaemonInfo() (types.Info, error) {
func (d *docker) DaemonInfo() (types.Info, error) {
return d.c.Info(context.Background())
}
func (d *docker) GetDaemonHost() string {
func (d *docker) DaemonHost() string {
return d.c.DaemonHost()
}
@@ -180,7 +187,7 @@ func (d *docker) GetPorts() ([]uint16, error) {
return openPorts, nil
}
func (d *docker) GetContainerStats(name string) (io.ReadCloser, error) {
func (d *docker) ContainerStats(name string) (io.ReadCloser, error) {
stats, err := d.c.ContainerStats(context.Background(), name, false)
return stats.Body, err
@@ -222,7 +229,21 @@ func (d *docker) CopyToContainer(containerName, destination, fileName string, co
return d.c.CopyToContainer(context.Background(), containerName, destination, r, types.CopyToContainerOptions{AllowOverwriteDirWithFile: true})
}
func (d *docker) DeleteContainer(name string) error {
func (d *docker) CopyFromContainer(containerName, filePath string) (io.Reader, error) {
rc, stat, err := d.c.CopyFromContainer(context.Background(), containerName, filePath)
if err != nil {
return nil, err
}
if stat.Mode.IsDir() {
return nil, fmt.Errorf("Copying directories is not supported")
}
tr := tar.NewReader(rc)
// advance to the only possible file in the tar archive
tr.Next()
return tr, nil
}
func (d *docker) ContainerDelete(name string) error {
err := d.c.ContainerRemove(context.Background(), name, types.ContainerRemoveOptions{Force: true, RemoveVolumes: true})
d.c.VolumeRemove(context.Background(), name, true)
return err
@@ -242,7 +263,7 @@ type CreateContainerOpts struct {
Networks []string
}
func (d *docker) CreateContainer(opts CreateContainerOpts) (err error) {
func (d *docker) ContainerCreate(opts CreateContainerOpts) (err error) {
// Make sure directories are available for the new instance container
containerDir := "/var/run/pwd"
containerCertDir := fmt.Sprintf("%s/certs", containerDir)
@@ -382,7 +403,7 @@ func (d *docker) CreateContainer(opts CreateContainerOpts) (err error) {
return
}
func (d *docker) GetContainerIPs(id string) (map[string]string, error) {
func (d *docker) ContainerIPs(id string) (map[string]string, error) {
cinfo, err := d.c.ContainerInspect(context.Background(), id)
if err != nil {
return nil, err
@@ -468,7 +489,7 @@ func (d *docker) Exec(instanceName string, command []string) (int, error) {
return ins.ExitCode, nil
}
func (d *docker) DisconnectNetwork(containerId, networkId string) error {
func (d *docker) NetworkDisconnect(containerId, networkId string) error {
err := d.c.NetworkDisconnect(context.Background(), networkId, containerId, true)
if err != nil {
@@ -480,7 +501,7 @@ func (d *docker) DisconnectNetwork(containerId, networkId string) error {
return nil
}
func (d *docker) DeleteNetwork(id string) error {
func (d *docker) NetworkDelete(id string) error {
err := d.c.NetworkRemove(context.Background(), id)
if err != nil {

View File

@@ -5,8 +5,8 @@ import (
"net"
"time"
"docker.io/go-docker/api/types"
client "docker.io/go-docker"
"docker.io/go-docker/api/types"
"github.com/stretchr/testify/mock"
)
@@ -19,12 +19,12 @@ func (m *Mock) GetClient() *client.Client {
return args.Get(0).(*client.Client)
}
func (m *Mock) CreateNetwork(id string, opts types.NetworkCreate) error {
func (m *Mock) NetworkCreate(id string, opts types.NetworkCreate) error {
args := m.Called(id, opts)
return args.Error(0)
}
func (m *Mock) ConnectNetwork(container, network, ip string) (string, error) {
func (m *Mock) NetworkConnect(container, network, ip string) (string, error) {
args := m.Called(container, network, ip)
return args.String(0), args.Error(1)
}
@@ -34,12 +34,12 @@ func (m *Mock) NetworkInspect(id string) (types.NetworkResource, error) {
return args.Get(0).(types.NetworkResource), args.Error(1)
}
func (m *Mock) GetDaemonInfo() (types.Info, error) {
func (m *Mock) DaemonInfo() (types.Info, error) {
args := m.Called()
return args.Get(0).(types.Info), args.Error(1)
}
func (m *Mock) GetDaemonHost() string {
func (m *Mock) DaemonHost() string {
args := m.Called()
return args.String(0)
}
@@ -53,7 +53,7 @@ func (m *Mock) GetPorts() ([]uint16, error) {
args := m.Called()
return args.Get(0).([]uint16), args.Error(1)
}
func (m *Mock) GetContainerStats(name string) (io.ReadCloser, error) {
func (m *Mock) ContainerStats(name string) (io.ReadCloser, error) {
args := m.Called(name)
return args.Get(0).(io.ReadCloser), args.Error(1)
}
@@ -73,15 +73,20 @@ func (m *Mock) CopyToContainer(containerName, destination, fileName string, cont
args := m.Called(containerName, destination, fileName, content)
return args.Error(0)
}
func (m *Mock) DeleteContainer(id string) error {
func (m *Mock) CopyFromContainer(containerName, filePath string) (io.Reader, error) {
args := m.Called(containerName, filePath)
return args.Get(0).(io.Reader), args.Error(1)
}
func (m *Mock) ContainerDelete(id string) error {
args := m.Called(id)
return args.Error(0)
}
func (m *Mock) CreateContainer(opts CreateContainerOpts) error {
func (m *Mock) ContainerCreate(opts CreateContainerOpts) error {
args := m.Called(opts)
return args.Error(0)
}
func (m *Mock) GetContainerIPs(id string) (map[string]string, error) {
func (m *Mock) ContainerIPs(id string) (map[string]string, error) {
args := m.Called(id)
return args.Get(0).(map[string]string), args.Error(1)
}
@@ -90,11 +95,11 @@ func (m *Mock) ExecAttach(instanceName string, command []string, out io.Writer)
args := m.Called(instanceName, command, out)
return args.Int(0), args.Error(1)
}
func (m *Mock) DisconnectNetwork(containerId, networkId string) error {
func (m *Mock) NetworkDisconnect(containerId, networkId string) error {
args := m.Called(containerId, networkId)
return args.Error(0)
}
func (m *Mock) DeleteNetwork(id string) error {
func (m *Mock) NetworkDelete(id string) error {
args := m.Called(id)
return args.Error(0)
}

View File

@@ -1,8 +0,0 @@
FROM franela/kind_builder
COPY motd /etc/motd
RUN echo $'cat /etc/motd \n\
export PS1="[\h \W]$ "' >> /root/.bash_profile
CMD systemctl start docker && systemctl start kubelet \
&& while true; do bash -l; done

View File

@@ -0,0 +1,2 @@
[url "https://"]
insteadOf = git://

73
dockerfiles/pwm/.inputrc Normal file
View File

@@ -0,0 +1,73 @@
# /etc/inputrc - global inputrc for libreadline
# See readline(3readline) and `info rluserman' for more information.
# Be 8 bit clean.
set input-meta on
set output-meta on
# To allow the use of 8bit-characters like the german umlauts, uncomment
# the line below. However this makes the meta key not work as a meta key,
# which is annoying to those which don't need to type in 8-bit characters.
# set convert-meta off
# try to enable the application keypad when it is called. Some systems
# need this to enable the arrow keys.
# set enable-keypad on
# see /usr/share/doc/bash/inputrc.arrows for other codes of arrow keys
# do not bell on tab-completion
# set bell-style none
# set bell-style visible
# some defaults / modifications for the emacs mode
$if mode=emacs
# allow the use of the Home/End keys
"\e[1~": beginning-of-line
"\e[4~": end-of-line
# allow the use of the Delete/Insert keys
"\e[3~": delete-char
"\e[2~": quoted-insert
# mappings for "page up" and "page down" to step to the beginning/end
# of the history
# "\e[5~": beginning-of-history
# "\e[6~": end-of-history
# alternate mappings for "page up" and "page down" to search the history
# "\e[5~": history-search-backward
# "\e[6~": history-search-forward
# mappings for Ctrl-left-arrow and Ctrl-right-arrow for word moving
"\e[1;5C": forward-word
"\e[1;5D": backward-word
"\e[5C": forward-word
"\e[5D": backward-word
"\e\e[C": forward-word
"\e\e[D": backward-word
$if term=rxvt
"\e[7~": beginning-of-line
"\e[8~": end-of-line
"\eOc": forward-word
"\eOd": backward-word
$endif
# for non RH/Debian xterm, can't hurt for RH/Debian xterm
# "\eOH": beginning-of-line
# "\eOF": end-of-line
# for freebsd console
# "\e[H": beginning-of-line
# "\e[F": end-of-line
$endif
# faster completion
set show-all-if-ambiguous on
"\e[A": history-search-backward
"\e[B": history-search-forward

5
dockerfiles/pwm/.profile Normal file
View File

@@ -0,0 +1,5 @@
export PS1='\e[1m\e[31m[\h] \e[32m\e[34m\u@$(hostname -i)\e[35m \w\e[0m\n$ '
alias vi='vim'
export PATH=$PATH:/root/go/bin
cat /etc/motd
echo $BASHPID > /var/run/cwd

6
dockerfiles/pwm/.vimrc Normal file
View File

@@ -0,0 +1,6 @@
syntax on
set autoindent
set expandtab
set number
set shiftwidth=2
set softtabstop=2

View File

@@ -0,0 +1,44 @@
ARG VERSION=docker:stable-dind
FROM ${VERSION}
RUN apk add --no-cache git tmux vim curl bash build-base qemu-img qemu-system-x86_64
ENV GOPATH /root/go
ENV PATH $PATH:$GOPATH
# Use specific moby commit due to vendoring mismatch
ENV MOBY_COMMIT="d9d2a91780b34b92e669bbfa099f613bd9fad6bb"
RUN mkdir /root/go && apk add --no-cache go \
&& go get -u -d github.com/moby/tool/cmd/moby && (cd $GOPATH/src/github.com/moby/tool/cmd/moby && git checkout $MOBY_COMMIT && go install) \
&& go get -u github.com/linuxkit/linuxkit/src/cmd/linuxkit \
&& rm -rf /root/go/pkg && rm -rf /root/go/src && rm -rf /usr/lib/go
# Add bash completion and set bash as default shell
RUN mkdir /etc/bash_completion.d \
&& curl https://raw.githubusercontent.com/docker/cli/master/contrib/completion/bash/docker -o /etc/bash_completion.d/docker \
&& sed -i "s/ash/bash/" /etc/passwd
# Replace modprobe with a no-op to get rid of spurious warnings
# (note: we can't just symlink to /bin/true because it might be busybox)
RUN rm /sbin/modprobe && echo '#!/bin/true' >/sbin/modprobe && chmod +x /sbin/modprobe
# Install a nice vimrc file and prompt (by soulshake)
COPY ["sudo", "/usr/local/bin/"]
COPY [".vimrc", ".profile", ".inputrc", ".gitconfig", "./root/"]
COPY ["motd", "/etc/motd"]
COPY ["daemon.json", "/etc/docker/"]
# Move to our home
WORKDIR /root
# Remove IPv6 alias for localhost and start docker in the background ...
CMD cat /etc/hosts >/etc/hosts.bak && \
sed 's/^::1.*//' /etc/hosts.bak > /etc/hosts && \
mount -t securityfs none /sys/kernel/security && \
dockerd &>/docker.log & \
while true ; do /bin/bash -l; done
# ... and then put a shell in the foreground, restarting it if it exits

View File

@@ -0,0 +1,7 @@
{
"experimental": true,
"debug": true,
"log-level": "info",
"insecure-registries": ["127.0.0.1"],
"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2375"]
}

8
dockerfiles/pwm/motd Normal file
View File

@@ -0,0 +1,8 @@
###############################################################
# WARNING!!!! #
# This is a sandbox environment. Using personal credentials #
# is HIGHLY! discouraged. Any consequences of doing so are #
# completely the user's responsibilites. #
# #
# The PWD team. #
###############################################################

5
dockerfiles/pwm/sudo Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
# This is shim to help with the case were pasted commands from a readme assume you are not root. Since this isto be run by root, it should effectively be a dummy command that allows the parameters to pass through.
exec $@

View File

@@ -68,8 +68,10 @@ func Register(extend HandlerExtender) {
corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}/uploads", FileUpload).Methods("POST")
corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}", DeleteInstance).Methods("DELETE")
corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}/exec", Exec).Methods("POST")
corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}/fstree", fsTree).Methods("GET")
corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}/file", file).Methods("GET")
r.HandleFunc("/sessions/{sessionId}/instances/{instanceName}/editor.html", func(rw http.ResponseWriter, r *http.Request) {
r.HandleFunc("/sessions/{sessionId}/instances/{instanceName}/editor", func(rw http.ResponseWriter, r *http.Request) {
http.ServeFile(rw, r, "www/editor.html")
})

53
handlers/file_instance.go Normal file
View File

@@ -0,0 +1,53 @@
package handlers
import (
"encoding/base64"
"io"
"log"
"net/http"
"github.com/gorilla/mux"
)
func file(rw http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
sessionId := vars["sessionId"]
instanceName := vars["instanceName"]
query := req.URL.Query()
path := query.Get("path")
if path == "" {
rw.WriteHeader(http.StatusBadRequest)
return
}
s, _ := core.SessionGet(sessionId)
if s == nil {
rw.WriteHeader(http.StatusNotFound)
return
}
i := core.InstanceGet(s, instanceName)
if i == nil {
rw.WriteHeader(http.StatusNotFound)
return
}
instanceFile, err := core.InstanceFile(i, path)
if err != nil {
log.Println(err)
rw.WriteHeader(http.StatusInternalServerError)
return
}
encoder := base64.NewEncoder(base64.StdEncoding, rw)
if _, err = io.Copy(encoder, instanceFile); err != nil {
log.Println(err)
rw.WriteHeader(http.StatusInternalServerError)
return
}
}

View File

@@ -0,0 +1,42 @@
package handlers
import (
"io"
"log"
"net/http"
"github.com/gorilla/mux"
)
func fsTree(rw http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
sessionId := vars["sessionId"]
instanceName := vars["instanceName"]
s, _ := core.SessionGet(sessionId)
if s == nil {
rw.WriteHeader(http.StatusNotFound)
return
}
i := core.InstanceGet(s, instanceName)
if i == nil {
rw.WriteHeader(http.StatusNotFound)
return
}
tree, err := core.InstanceFSTree(i)
if err != nil {
log.Println(err)
rw.WriteHeader(http.StatusInternalServerError)
return
}
rw.Header().Set("content-type", "application/json")
if _, err = io.Copy(rw, tree); err != nil {
log.Println(err)
rw.WriteHeader(http.StatusInternalServerError)
return
}
}

View File

@@ -83,11 +83,11 @@ func (d *DinD) InstanceNew(session *types.Session, conf types.InstanceConfig) (*
if err != nil {
return nil, err
}
if err := dockerClient.CreateContainer(opts); err != nil {
if err := dockerClient.ContainerCreate(opts); err != nil {
return nil, err
}
ips, err := dockerClient.GetContainerIPs(containerName)
ips, err := dockerClient.ContainerIPs(containerName)
if err != nil {
return nil, err
}
@@ -131,7 +131,7 @@ func (d *DinD) InstanceDelete(session *types.Session, instance *types.Instance)
if err != nil {
return err
}
err = dockerClient.DeleteContainer(instance.Name)
err = dockerClient.ContainerDelete(instance.Name)
if err != nil && !strings.Contains(err.Error(), "No such container") {
return err
}
@@ -150,6 +150,40 @@ func (d *DinD) InstanceExec(instance *types.Instance, cmd []string) (int, error)
return dockerClient.Exec(instance.Name, cmd)
}
func (d *DinD) InstanceFSTree(instance *types.Instance) (io.Reader, error) {
session, err := d.getSession(instance.SessionId)
if err != nil {
return nil, err
}
dockerClient, err := d.factory.GetForSession(session)
if err != nil {
return nil, err
}
b := bytes.NewBuffer([]byte{})
if c, err := dockerClient.ExecAttach(instance.Name, []string{"bash", "-c", `tree --noreport -J $HOME`}, b); c > 0 {
log.Println(b.String())
return nil, fmt.Errorf("Error %d trying list directories", c)
} else if err != nil {
return nil, err
}
return b, nil
}
func (d *DinD) InstanceFile(instance *types.Instance, filePath string) (io.Reader, error) {
session, err := d.getSession(instance.SessionId)
if err != nil {
return nil, err
}
dockerClient, err := d.factory.GetForSession(session)
if err != nil {
return nil, err
}
return dockerClient.CopyFromContainer(instance.Name, filePath)
}
func (d *DinD) InstanceResizeTerminal(instance *types.Instance, rows, cols uint) error {
session, err := d.getSession(instance.SessionId)
if err != nil {

View File

@@ -27,7 +27,7 @@ func (p *overlaySessionProvisioner) SessionNew(ctx context.Context, s *types.Ses
// We assume we are out of capacity
return fmt.Errorf("Out of capacity")
}
u, _ := url.Parse(dockerClient.GetDaemonHost())
u, _ := url.Parse(dockerClient.DaemonHost())
if u.Host == "" {
s.Host = "localhost"
} else {
@@ -36,13 +36,13 @@ func (p *overlaySessionProvisioner) SessionNew(ctx context.Context, s *types.Ses
}
opts := dtypes.NetworkCreate{Driver: "overlay", Attachable: true}
if err := dockerClient.CreateNetwork(s.Id, opts); err != nil {
if err := dockerClient.NetworkCreate(s.Id, opts); err != nil {
log.Println("ERROR NETWORKING", err)
return err
}
log.Printf("Network [%s] created for session [%s]\n", s.Id, s.Id)
ip, err := dockerClient.ConnectNetwork(config.L2ContainerName, s.Id, s.PwdIpAddress)
ip, err := dockerClient.NetworkConnect(config.L2ContainerName, s.Id, s.PwdIpAddress)
if err != nil {
log.Println(err)
return err
@@ -58,14 +58,14 @@ func (p *overlaySessionProvisioner) SessionClose(s *types.Session) error {
log.Println(err)
return err
}
if err := dockerClient.DisconnectNetwork(config.L2ContainerName, s.Id); err != nil {
if err := dockerClient.NetworkDisconnect(config.L2ContainerName, s.Id); err != nil {
if !strings.Contains(err.Error(), "is not connected to the network") {
log.Println("ERROR NETWORKING", err)
return err
}
}
log.Printf("Disconnected l2 from network [%s]\n", s.Id)
if err := dockerClient.DeleteNetwork(s.Id); err != nil {
if err := dockerClient.NetworkDelete(s.Id); err != nil {
if !strings.Contains(err.Error(), "not found") {
log.Println(err)
return err

View File

@@ -19,6 +19,8 @@ type InstanceProvisionerApi interface {
InstanceNew(session *types.Session, conf types.InstanceConfig) (*types.Instance, error)
InstanceDelete(session *types.Session, instance *types.Instance) error
InstanceExec(instance *types.Instance, cmd []string) (int, error)
InstanceFSTree(instance *types.Instance) (io.Reader, error)
InstanceFile(instance *types.Instance, filePath string) (io.Reader, error)
InstanceResizeTerminal(instance *types.Instance, cols, rows uint) error
InstanceGetTerminal(instance *types.Instance) (net.Conn, error)

View File

@@ -155,6 +155,15 @@ func (d *windows) InstanceExec(instance *types.Instance, cmd []string) (int, err
return ex.ExitCode, nil
}
func (d *windows) InstanceFSTree(instance *types.Instance) (io.Reader, error) {
//TODO implement
return nil, nil
}
func (d *windows) InstanceFile(instance *types.Instance, filePath string) (io.Reader, error) {
//TODO implement
return nil, nil
}
func (d *windows) releaseInstance(instanceId string) error {
return d.storage.WindowsInstanceDelete(instanceId)
}

View File

@@ -29,9 +29,9 @@ func TestClientNew(t *testing.T) {
_g.On("NewId").Return("aaaabbbbcccc")
_f.On("GetForSession", mock.AnythingOfType("*types.Session")).Return(_d, nil)
_d.On("CreateNetwork", "aaaabbbbcccc", dtypes.NetworkCreate{Attachable: true, Driver: "overlay"}).Return(nil)
_d.On("GetDaemonHost").Return("localhost")
_d.On("ConnectNetwork", config.L2ContainerName, "aaaabbbbcccc", "").Return("10.0.0.1", nil)
_d.On("NetworkCreate", "aaaabbbbcccc", dtypes.NetworkCreate{Attachable: true, Driver: "overlay"}).Return(nil)
_d.On("DaemonHost").Return("localhost")
_d.On("NetworkConnect", config.L2ContainerName, "aaaabbbbcccc", "").Return("10.0.0.1", nil)
_s.On("SessionPut", mock.AnythingOfType("*types.Session")).Return(nil)
_s.On("SessionCount").Return(1, nil)
_s.On("InstanceCount").Return(0, nil)
@@ -72,9 +72,9 @@ func TestClientCount(t *testing.T) {
_g.On("NewId").Return("aaaabbbbcccc")
_f.On("GetForSession", mock.AnythingOfType("*types.Session")).Return(_d, nil)
_d.On("CreateNetwork", "aaaabbbbcccc", dtypes.NetworkCreate{Attachable: true, Driver: "overlay"}).Return(nil)
_d.On("GetDaemonHost").Return("localhost")
_d.On("ConnectNetwork", config.L2ContainerName, "aaaabbbbcccc", "").Return("10.0.0.1", nil)
_d.On("NetworkCreate", "aaaabbbbcccc", dtypes.NetworkCreate{Attachable: true, Driver: "overlay"}).Return(nil)
_d.On("DaemonHost").Return("localhost")
_d.On("NetworkConnect", config.L2ContainerName, "aaaabbbbcccc", "").Return("10.0.0.1", nil)
_s.On("SessionPut", mock.AnythingOfType("*types.Session")).Return(nil)
_s.On("ClientPut", mock.AnythingOfType("*types.Client")).Return(nil)
_s.On("ClientCount").Return(1, nil)
@@ -113,9 +113,9 @@ func TestClientResizeViewPort(t *testing.T) {
_g.On("NewId").Return("aaaabbbbcccc")
_f.On("GetForSession", mock.AnythingOfType("*types.Session")).Return(_d, nil)
_d.On("CreateNetwork", "aaaabbbbcccc", dtypes.NetworkCreate{Attachable: true, Driver: "overlay"}).Return(nil)
_d.On("GetDaemonHost").Return("localhost")
_d.On("ConnectNetwork", config.L2ContainerName, "aaaabbbbcccc", "").Return("10.0.0.1", nil)
_d.On("NetworkCreate", "aaaabbbbcccc", dtypes.NetworkCreate{Attachable: true, Driver: "overlay"}).Return(nil)
_d.On("DaemonHost").Return("localhost")
_d.On("NetworkConnect", config.L2ContainerName, "aaaabbbbcccc", "").Return("10.0.0.1", nil)
_s.On("SessionPut", mock.AnythingOfType("*types.Session")).Return(nil)
_s.On("SessionCount").Return(1, nil)
_s.On("InstanceCount").Return(0, nil)

View File

@@ -149,3 +149,23 @@ func (p *pwd) InstanceExec(instance *types.Instance, cmd []string) (int, error)
}
return exitCode, nil
}
func (p *pwd) InstanceFSTree(instance *types.Instance) (io.Reader, error) {
defer observeAction("InstanceFSTree", time.Now())
prov, err := p.getProvisioner(instance.Type)
if err != nil {
return nil, err
}
return prov.InstanceFSTree(instance)
}
func (p *pwd) InstanceFile(instance *types.Instance, filePath string) (io.Reader, error) {
defer observeAction("InstanceFile", time.Now())
prov, err := p.getProvisioner(instance.Type)
if err != nil {
return nil, err
}
return prov.InstanceFile(instance, filePath)
}

View File

@@ -56,9 +56,9 @@ func TestInstanceNew(t *testing.T) {
_g.On("NewId").Return("aaaabbbbcccc")
_f.On("GetForSession", mock.AnythingOfType("*types.Session")).Return(_d, nil)
_d.On("CreateNetwork", "aaaabbbbcccc", dtypes.NetworkCreate{Attachable: true, Driver: "overlay"}).Return(nil)
_d.On("GetDaemonHost").Return("localhost")
_d.On("ConnectNetwork", config.L2ContainerName, "aaaabbbbcccc", "").Return("10.0.0.1", nil)
_d.On("NetworkCreate", "aaaabbbbcccc", dtypes.NetworkCreate{Attachable: true, Driver: "overlay"}).Return(nil)
_d.On("DaemonHost").Return("localhost")
_d.On("NetworkConnect", config.L2ContainerName, "aaaabbbbcccc", "").Return("10.0.0.1", nil)
_s.On("SessionPut", mock.AnythingOfType("*types.Session")).Return(nil)
_s.On("SessionCount").Return(1, nil)
_s.On("ClientCount").Return(0, nil)
@@ -101,8 +101,8 @@ func TestInstanceNew(t *testing.T) {
HostFQDN: "something.play-with-docker.com",
Networks: []string{session.Id},
}
_d.On("CreateContainer", expectedContainerOpts).Return(nil)
_d.On("GetContainerIPs", expectedInstance.Name).Return(map[string]string{session.Id: "10.0.0.1"}, nil)
_d.On("ContainerCreate", expectedContainerOpts).Return(nil)
_d.On("ContainerIPs", expectedInstance.Name).Return(map[string]string{session.Id: "10.0.0.1"}, nil)
_s.On("InstancePut", mock.AnythingOfType("*types.Instance")).Return(nil)
_e.M.On("Emit", event.INSTANCE_NEW, "aaaabbbbcccc", []interface{}{"aaaabbbb_aaaabbbbcccc", "10.0.0.1", "node1", "ip10-0-0-1-aaaabbbbcccc"}).Return()
@@ -129,9 +129,9 @@ func TestInstanceNew_WithNotAllowedImage(t *testing.T) {
_g.On("NewId").Return("aaaabbbbcccc")
_f.On("GetForSession", mock.AnythingOfType("*types.Session")).Return(_d, nil)
_d.On("CreateNetwork", "aaaabbbbcccc", dtypes.NetworkCreate{Attachable: true, Driver: "overlay"}).Return(nil)
_d.On("GetDaemonHost").Return("localhost")
_d.On("ConnectNetwork", config.L2ContainerName, "aaaabbbbcccc", "").Return("10.0.0.1", nil)
_d.On("NetworkCreate", "aaaabbbbcccc", dtypes.NetworkCreate{Attachable: true, Driver: "overlay"}).Return(nil)
_d.On("DaemonHost").Return("localhost")
_d.On("NetworkConnect", config.L2ContainerName, "aaaabbbbcccc", "").Return("10.0.0.1", nil)
_s.On("SessionPut", mock.AnythingOfType("*types.Session")).Return(nil)
_s.On("SessionCount").Return(1, nil)
_s.On("ClientCount").Return(0, nil)
@@ -171,8 +171,8 @@ func TestInstanceNew_WithNotAllowedImage(t *testing.T) {
Privileged: true,
Networks: []string{session.Id},
}
_d.On("CreateContainer", expectedContainerOpts).Return(nil)
_d.On("GetContainerIPs", expectedInstance.Name).Return(map[string]string{session.Id: "10.0.0.1"}, nil)
_d.On("ContainerCreate", expectedContainerOpts).Return(nil)
_d.On("ContainerIPs", expectedInstance.Name).Return(map[string]string{session.Id: "10.0.0.1"}, nil)
_s.On("InstancePut", mock.AnythingOfType("*types.Instance")).Return(nil)
_e.M.On("Emit", event.INSTANCE_NEW, "aaaabbbbcccc", []interface{}{"aaaabbbb_aaaabbbbcccc", "10.0.0.1", "node1", "ip10-0-0-1-aaaabbbbcccc"}).Return()
@@ -200,9 +200,9 @@ func TestInstanceNew_WithCustomHostname(t *testing.T) {
_g.On("NewId").Return("aaaabbbbcccc")
_f.On("GetForSession", mock.AnythingOfType("*types.Session")).Return(_d, nil)
_d.On("CreateNetwork", "aaaabbbbcccc", dtypes.NetworkCreate{Attachable: true, Driver: "overlay"}).Return(nil)
_d.On("GetDaemonHost").Return("localhost")
_d.On("ConnectNetwork", config.L2ContainerName, "aaaabbbbcccc", "").Return("10.0.0.1", nil)
_d.On("NetworkCreate", "aaaabbbbcccc", dtypes.NetworkCreate{Attachable: true, Driver: "overlay"}).Return(nil)
_d.On("DaemonHost").Return("localhost")
_d.On("NetworkConnect", config.L2ContainerName, "aaaabbbbcccc", "").Return("10.0.0.1", nil)
_s.On("SessionPut", mock.AnythingOfType("*types.Session")).Return(nil)
_s.On("SessionCount").Return(1, nil)
_s.On("ClientCount").Return(0, nil)
@@ -242,8 +242,8 @@ func TestInstanceNew_WithCustomHostname(t *testing.T) {
Networks: []string{session.Id},
}
_d.On("CreateContainer", expectedContainerOpts).Return(nil)
_d.On("GetContainerIPs", expectedInstance.Name).Return(map[string]string{session.Id: "10.0.0.1"}, nil)
_d.On("ContainerCreate", expectedContainerOpts).Return(nil)
_d.On("ContainerIPs", expectedInstance.Name).Return(map[string]string{session.Id: "10.0.0.1"}, nil)
_s.On("InstancePut", mock.AnythingOfType("*types.Instance")).Return(nil)
_e.M.On("Emit", event.INSTANCE_NEW, "aaaabbbbcccc", []interface{}{"aaaabbbb_aaaabbbbcccc", "10.0.0.1", "redis-master", "ip10-0-0-1-aaaabbbbcccc"}).Return()

View File

@@ -87,6 +87,16 @@ func (m *Mock) InstanceExec(instance *types.Instance, cmd []string) (int, error)
return args.Int(0), args.Error(1)
}
func (m *Mock) InstanceFSTree(instance *types.Instance) (io.Reader, error) {
args := m.Called(instance)
return args.Get(0).(io.Reader), args.Error(1)
}
func (m *Mock) InstanceFile(instance *types.Instance, filePath string) (io.Reader, error) {
args := m.Called(instance, filePath)
return args.Get(0).(io.Reader), args.Error(1)
}
func (m *Mock) ClientNew(id string, session *types.Session) *types.Client {
args := m.Called(id, session)
return args.Get(0).(*types.Client)

View File

@@ -89,6 +89,8 @@ type PWDApi interface {
InstanceFindBySession(session *types.Session) ([]*types.Instance, error)
InstanceDelete(session *types.Session, instance *types.Instance) error
InstanceExec(instance *types.Instance, cmd []string) (int, error)
InstanceFSTree(instance *types.Instance) (io.Reader, error)
InstanceFile(instance *types.Instance, filePath string) (io.Reader, error)
ClientNew(id string, session *types.Session) *types.Client
ClientResizeViewPort(client *types.Client, cols, rows uint)

View File

@@ -31,9 +31,9 @@ func TestSessionNew(t *testing.T) {
_g.On("NewId").Return("aaaabbbbcccc")
_f.On("GetForSession", mock.AnythingOfType("*types.Session")).Return(_d, nil)
_d.On("CreateNetwork", "aaaabbbbcccc", dtypes.NetworkCreate{Attachable: true, Driver: "overlay"}).Return(nil)
_d.On("GetDaemonHost").Return("localhost")
_d.On("ConnectNetwork", config.L2ContainerName, "aaaabbbbcccc", "").Return("10.0.0.1", nil)
_d.On("NetworkCreate", "aaaabbbbcccc", dtypes.NetworkCreate{Attachable: true, Driver: "overlay"}).Return(nil)
_d.On("DaemonHost").Return("localhost")
_d.On("NetworkConnect", config.L2ContainerName, "aaaabbbbcccc", "").Return("10.0.0.1", nil)
_s.On("SessionPut", mock.AnythingOfType("*types.Session")).Return(nil)
_s.On("SessionCount").Return(1, nil)
_s.On("InstanceCount").Return(0, nil)
@@ -93,9 +93,9 @@ func TestSessionSetup(t *testing.T) {
_g.On("NewId").Return("aaaabbbbcccc")
_f.On("GetForSession", "aaaabbbbcccc").Return(_d, nil)
_d.On("CreateNetwork", "aaaabbbbcccc", dtypes.NetworkCreate{Attachable: true, Driver: "overlay"}).Return(nil)
_d.On("GetDaemonHost").Return("localhost")
_d.On("ConnectNetwork", config.L2ContainerName, "aaaabbbbcccc", "").Return("10.0.0.1", nil)
_d.On("NetworkCreate", "aaaabbbbcccc", dtypes.NetworkCreate{Attachable: true, Driver: "overlay"}).Return(nil)
_d.On("DaemonHost").Return("localhost")
_d.On("NetworkConnect", config.L2ContainerName, "aaaabbbbcccc", "").Return("10.0.0.1", nil)
_s.On("SessionPut", mock.AnythingOfType("*types.Session")).Return(nil)
_s.On("InstancePut", mock.AnythingOfType("*types.Instance")).Return(nil)
_s.On("SessionCount").Return(1, nil)
@@ -104,31 +104,31 @@ func TestSessionSetup(t *testing.T) {
_s.On("InstanceFindBySessionId", "aaaabbbbcccc").Return([]*types.Instance{}, nil)
_d.On("CreateContainer", docker.CreateContainerOpts{Image: "franela/dind", SessionId: "aaaabbbbcccc", ContainerName: "aaaabbbb_manager1", Hostname: "manager1", Privileged: true, HostFQDN: "localhost", Networks: []string{"aaaabbbbcccc"}}).Return(nil)
_d.On("GetContainerIPs", "aaaabbbb_manager1").Return(map[string]string{"aaaabbbbcccc": "10.0.0.2"}, nil)
_d.On("ContainerIPs", "aaaabbbb_manager1").Return(map[string]string{"aaaabbbbcccc": "10.0.0.2"}, nil)
_f.On("GetForInstance", mock.AnythingOfType("*types.Instance")).Return(_d, nil)
_d.On("SwarmInit").Return(&docker.SwarmTokens{Manager: "managerToken", Worker: "workerToken"}, nil)
_e.M.On("Emit", event.INSTANCE_NEW, "aaaabbbbcccc", []interface{}{"aaaabbbb_manager1", "10.0.0.2", "manager1", "ip10-0-0-2-aaaabbbbcccc"}).Return()
_d.On("CreateContainer", docker.CreateContainerOpts{Image: "franela/dind", SessionId: "aaaabbbbcccc", ContainerName: "aaaabbbb_manager2", Hostname: "manager2", Privileged: true, HostFQDN: "localhost", Networks: []string{"aaaabbbbcccc"}}).Return(nil)
_d.On("GetContainerIPs", "aaaabbbb_manager2").Return(map[string]string{"aaaabbbbcccc": "10.0.0.3"}, nil)
_d.On("ContainerIPs", "aaaabbbb_manager2").Return(map[string]string{"aaaabbbbcccc": "10.0.0.3"}, nil)
_f.On("GetForInstance", mock.AnythingOfType("*types.Instance")).Return(_d, nil)
_d.On("SwarmJoin", "10.0.0.2:2377", "managerToken").Return(nil)
_e.M.On("Emit", event.INSTANCE_NEW, "aaaabbbbcccc", []interface{}{"aaaabbbb_manager2", "10.0.0.3", "manager2", "ip10-0-0-3-aaaabbbbcccc"}).Return()
_d.On("CreateContainer", docker.CreateContainerOpts{Image: "franela/dind:overlay2-dev", SessionId: "aaaabbbbcccc", ContainerName: "aaaabbbb_manager3", Hostname: "manager3", Privileged: true, HostFQDN: "localhost", Networks: []string{"aaaabbbbcccc"}}).Return(nil)
_d.On("GetContainerIPs", "aaaabbbb_manager3").Return(map[string]string{"aaaabbbbcccc": "10.0.0.4"}, nil)
_d.On("ContainerIPs", "aaaabbbb_manager3").Return(map[string]string{"aaaabbbbcccc": "10.0.0.4"}, nil)
_f.On("GetForInstance", mock.AnythingOfType("*types.Instance")).Return(_d, nil)
_d.On("SwarmJoin", "10.0.0.2:2377", "managerToken").Return(nil)
_e.M.On("Emit", event.INSTANCE_NEW, "aaaabbbbcccc", []interface{}{"aaaabbbb_manager3", "10.0.0.4", "manager3", "ip10-0-0-4-aaaabbbbcccc"}).Return()
_d.On("CreateContainer", docker.CreateContainerOpts{Image: "franela/dind", SessionId: "aaaabbbbcccc", ContainerName: "aaaabbbb_worker1", Hostname: "worker1", Privileged: true, HostFQDN: "localhost", Networks: []string{"aaaabbbbcccc"}}).Return(nil)
_d.On("GetContainerIPs", "aaaabbbb_worker1").Return(map[string]string{"aaaabbbbcccc": "10.0.0.5"}, nil)
_d.On("ContainerIPs", "aaaabbbb_worker1").Return(map[string]string{"aaaabbbbcccc": "10.0.0.5"}, nil)
_f.On("GetForInstance", mock.AnythingOfType("*types.Instance")).Return(_d, nil)
_d.On("SwarmJoin", "10.0.0.2:2377", "workerToken").Return(nil)
_e.M.On("Emit", event.INSTANCE_NEW, "aaaabbbbcccc", []interface{}{"aaaabbbb_worker1", "10.0.0.5", "worker1", "ip10-0-0-5-aaaabbbbcccc"}).Return()
_d.On("CreateContainer", docker.CreateContainerOpts{Image: "franela/dind", SessionId: "aaaabbbbcccc", ContainerName: "aaaabbbb_other", Hostname: "other", Privileged: true, HostFQDN: "localhost", Networks: []string{"aaaabbbbcccc"}}).Return(nil)
_d.On("GetContainerIPs", "aaaabbbb_other").Return(map[string]string{"aaaabbbbcccc": "10.0.0.6"}, nil)
_d.On("ContainerIPs", "aaaabbbb_other").Return(map[string]string{"aaaabbbbcccc": "10.0.0.6"}, nil)
_e.M.On("Emit", event.INSTANCE_NEW, "aaaabbbbcccc", []interface{}{"aaaabbbb_other", "10.0.0.6", "other", "ip10-0-0-6-aaaabbbbcccc"}).Return()
var nilArgs []interface{}

View File

@@ -42,7 +42,7 @@ func TestCheckSwarmPorts_RunWhenManager(t *testing.T) {
}
f.On("GetForInstance", i).Return(d, nil)
d.On("GetDaemonInfo").Return(info, nil)
d.On("DaemonInfo").Return(info, nil)
d.On("GetSwarmPorts").Return([]string{"aaaabbbb_node1", "aaaabbbb_node2"}, []uint16{8080, 9090}, nil)
e.M.On("Emit", CheckSwarmPortsEvent, "aaaabbbbcccc", []interface{}{ClusterPorts{Manager: i.Name, Instances: []string{i.Name, "aaaabbbb_node2"}, Ports: []int{8080, 9090}}}).Return()

View File

@@ -49,7 +49,7 @@ func NewCheckSwarmStatus(e event.EventApi, f docker.FactoryApi) *checkSwarmStatu
func getDockerSwarmStatus(ctx context.Context, client docker.DockerApi) (ClusterStatus, error) {
status := ClusterStatus{}
info, err := client.GetDaemonInfo()
info, err := client.DaemonInfo()
if err != nil {
return status, err
}

View File

@@ -40,7 +40,7 @@ func TestCheckSwarmStatus_RunWhenInactive(t *testing.T) {
}
f.On("GetForInstance", i).Return(d, nil)
d.On("GetDaemonInfo").Return(infoInactive, nil)
d.On("DaemonInfo").Return(infoInactive, nil)
e.M.On("Emit", CheckSwarmStatusEvent, "aaabbbccc", []interface{}{ClusterStatus{IsManager: false, IsWorker: false, Instance: "node1"}}).Return()
task := NewCheckSwarmStatus(e, f)
@@ -71,7 +71,7 @@ func TestCheckSwarmStatus_RunWhenLocked(t *testing.T) {
}
f.On("GetForInstance", i).Return(d, nil)
d.On("GetDaemonInfo").Return(infoLocked, nil)
d.On("DaemonInfo").Return(infoLocked, nil)
e.M.On("Emit", CheckSwarmStatusEvent, "aaabbbccc", []interface{}{ClusterStatus{IsManager: false, IsWorker: false, Instance: "node1"}}).Return()
task := NewCheckSwarmStatus(e, f)
@@ -103,7 +103,7 @@ func TestCheckSwarmStatus_RunWhenManager(t *testing.T) {
}
f.On("GetForInstance", i).Return(d, nil)
d.On("GetDaemonInfo").Return(infoLocked, nil)
d.On("DaemonInfo").Return(infoLocked, nil)
e.M.On("Emit", CheckSwarmStatusEvent, "aaabbbccc", []interface{}{ClusterStatus{IsManager: true, IsWorker: false, Instance: "node1"}}).Return()
task := NewCheckSwarmStatus(e, f)
@@ -135,7 +135,7 @@ func TestCheckSwarmStatus_RunWhenWorker(t *testing.T) {
}
f.On("GetForInstance", i).Return(d, nil)
d.On("GetDaemonInfo").Return(infoLocked, nil)
d.On("DaemonInfo").Return(infoLocked, nil)
e.M.On("Emit", CheckSwarmStatusEvent, "aaabbbccc", []interface{}{ClusterStatus{IsManager: false, IsWorker: true, Instance: "node1"}}).Return()
task := NewCheckSwarmStatus(e, f)

View File

@@ -91,7 +91,7 @@ func (t *collectStats) Run(ctx context.Context, instance *types.Instance) error
log.Println(err)
return err
}
reader, err := dockerClient.GetContainerStats(instance.Name)
reader, err := dockerClient.ContainerStats(instance.Name)
if err != nil {
log.Println("Error while trying to collect instance stats", err)
return err

View File

@@ -65,7 +65,7 @@ func TestCollectStats_Run(t *testing.T) {
s.On("SessionGet", i.SessionId).Return(sess, nil)
f.On("GetForSession", sess).Return(d, nil)
d.On("GetContainerStats", i.Name).Return(nopCloser{bytes.NewReader(b)}, nil)
d.On("ContainerStats", i.Name).Return(nopCloser{bytes.NewReader(b)}, nil)
e.M.On("Emit", CollectStatsEvent, "aaaabbbbcccc", []interface{}{InstanceStats{Instance: i.Name, Mem: "0.00% (0B / 0B)", Cpu: "0.00%"}}).Return()
task := NewCollectStats(e, f, s)

15
www/assets/editor.css Normal file
View File

@@ -0,0 +1,15 @@
.alert-top {
position: absolute;
top: 0;
right: 0;
width:100px;
display:none;
text-align: center;
padding: 3px;
height: 30px;
margin-bottom: 0px;
}
.col-md-3 {
overflow-x: auto;
}

171
www/editor.html Normal file
View File

@@ -0,0 +1,171 @@
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>Editor</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/zTree.v3/3.5.29/css/metroStyle/metroStyle.min.css" type="text/css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/open-iconic/1.1.1/font/css/open-iconic-bootstrap.min.css">
<link rel="stylesheet" href="/assets/editor.css">
</head>
<body>
<div class="container-fluid ">
<div id="alert-info" class="alert alert-info alert-top" role="alert">
<span class="alert-msg"></span>
</div>
<div class="row" style="height: 100%">
<div class="col-md-3">
<div>
<button type="button" id='treeReloadBtn' class="btn btn-sm">
<span class="oi oi-reload" title="Refresh" aria-hidden="true"></span>
</button>
</div>
<div id="fileTree" class="ztree"></div>
</div>
<div class="col-md-8">
<!-- Nav tabs -->
<ul class="nav nav-tabs" id="tabs" role="tablist">
</ul>
<!-- Tab panes -->
<div class="tab-content">
</div>
</div>
</div>
</div>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script type="text/javascript" src="https://cdn.rawgit.com/zTree/zTree_v3/4f2717d4/js/jquery.ztree.core.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js" integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js" integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.9/ace.js"></script>
<script src="https://cdn.rawgit.com/beatgammit/base64-js/be644dec/base64js.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
var alertShow = function(msg) {
$div = $('.alert-info');
$div.find('.alert-msg').text(msg);
if ($div.css('display') === 'none') {
// fadein, fadeout.
$div.fadeIn(1000).delay(1000).fadeOut(1000);
}
};
//regiters events for newly created tab elements
var registerEvents = function(tabId) {
//this method will register event on close icon on the tab..
$(".closeTab").click(function () {
//there are multiple elements which has .closeTab icon so close the tab whose close icon is clicked
var tabContentId = $(this).parent().attr("href");
$(this).parent().parent().remove(); //remove li of tab
$('#tabs a:last').tab('show'); // Select first tab
$(tabContentId).remove(); //remove respective tab content
});
$('#fileReload_'+tabId+'Btn').click(function() {
var path = $(this).attr('data-file-path');
loadFile(path, tabId);
});
$('#fileSave_'+tabId+'Btn').click(function() {
var path = $(this).attr('data-file-path');
saveFile(path, tabId);
});
};
var getFilePath = function(treeNode) {
var parent = treeNode.getParentNode();
var path = '';
if (parent) {
return getFilePath(parent) + '/' + treeNode.name;
} else {
return treeNode.name;
}
};
var treeClick = function(event, treeId, treeNode, clickFlag) {
if (!treeNode.isParent) {
var tabId = treeNode.tId;
if ($('#tab_' + tabId + '').length == 0) {
var filePath = getFilePath(treeNode);
$('.nav-tabs').append('<li class="nav-item"><a class="nav-link" data-toggle="tab" role="tab" href="#tab_' + tabId + '"><button class="close closeTab" type="button" >×</button>'+treeNode.name+'</a></li>');
$('.tab-content').append(' \
<div class="tab-pane" id="tab_' + tabId + '"> \
<div style="height: 40px; width: 100%; padding-top: 5px; background-color: #F0F0F0"> \
<button type="button" id="fileSave_'+tabId+'Btn" data-file-path="'+filePath+'" class="btn btn-info btn-sm"> \
<span class="oi oi-data-transfer-upload" title="Save" aria-hidden="true"></span> Save \
</button> \
<button type="button" id="fileReload_'+tabId+'Btn" data-file-path="'+filePath+'" class="btn btn-info btn-sm"> \
<span class="oi oi-reload" title="Save" aria-hidden="true"></span> Reload \
</button> \
</div> \
<div id="editor_'+ tabId +'" style="height: calc(100vh - 82px); width: 100%;"></div> \
</div> \
');
loadFile(filePath, tabId);
registerEvents(tabId);
}
$('#tabs a[href="#tab_'+ tabId +'"]').tab('show');
}
};
var loadFile = function(filePath, tabId) {
var editor = ace.edit('editor_'+tabId);
$.get('./file?path='+filePath)
.done(function( fileBase64 ) {
var bytes = base64js.toByteArray(fileBase64);
editor.setValue((new TextDecoder("utf-8")).decode(bytes), -1);
editor.focus();
alertShow('file loaded')
});
}
var saveFile = function(filePath, tabId) {
var editor = ace.edit('editor_'+tabId);
var fileData = new Blob([editor.getValue()], { type: 'text/plain' });
var data = new FormData();
var fileName = filePath.substr(filePath.lastIndexOf('/'))
data.append(fileName,fileData, fileName);
$.ajax({
url: 'uploads?path='+filePath.substr(0, filePath.lastIndexOf('/')),
data: data,
cache: false,
contentType: false,
processData: false,
method: 'POST',
type: 'POST', // For jQuery < 1.9
success: function(data) {
alertShow('file saved')
}
});
};
var setting = {
data: {
key: {
children: "contents"
}
},
callback: {
onClick: treeClick
}
};
var populateTree = function() {
$.getJSON('./fstree')
.done(function( treeData ) {
treeData[0].open = true;
$.fn.zTree.init($("#fileTree"), setting, treeData);
});
}
// Attach handlers to tree reload btn
$('#treeReloadBtn').click(populateTree);
// populate tree whenever the page starts
populateTree();
});
</script>
</body>
</html>