Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
anikitenko committed Jun 5, 2018
0 parents commit 9914b3e
Show file tree
Hide file tree
Showing 40 changed files with 2,575 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
test-results/
tmp/
routes/
vendor/*/
.idea
.DS_Store
node_modules
log/
90 changes: 90 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Mini sFTP client

This is a mini web based sFTP client written on Go using Revel Framework

## Quick Start

### Download and run

* Access [releases page](https://github.com/anikitenko/mini-sftp-client/releases)
and pickup the latest version for your OS
* Download and unzip archive locally
* Run run.bat for Windows OR run.sh for Linux/OS X
* When prompted enter port to listen on
* Access http://127.0.0.1:[port you choose]

### Run from sources

Prerequisite:

* Go 1.6+

Install Revel:

go get -u github.com/revel/cmd/revel

Get mini sftp client:

go get -u github.com/anikitenko/mini-sftp-client

Run app:

revel run github.com/anikitenko/mini-sftp-client

### Benefits and Key features
- [x] Nothing to install: unzip and run. Use different tabs for different connections
- [x] Runs on Linux, OS X, Windows
- [x] Run client for all interfaces and access client from mobile device and manage files
- [x] Run client on your file server (possible Linux based, Windows, OS X) and access from your desktop or mobile

## Usage

Once you navigate to http://127.0.0.1:[port you choose] you should see the following screen:

[[https://github.com/anikitenko/mini-sftp-client/blob/master/doc-images/first-screen.png|alt=first screen]]

##### Notes:
* If you are able to authenticate without password on your server, you may ignore that field
* During establishing SSH connection client will try to use .ssh/id_rsa and .ssh/id_dsa if client finds them
* Unsure about connection? Use Test button
* Changing connection name also changes title of the page.
Open client in a couple of tabs, change connection name and
you will be able to distinguish different connections

##### Establishing connection:

[[https://github.com/anikitenko/mini-sftp-client/blob/master/doc-images/connecting.png|alt=connecting]]

##### Notes:
* After you successfully established connection, client will try to detect home directories no matter what is your local OS
* Button to Test connection is disabled after successful connection
* This is because you can enter credentials to 1 server and if you test
for another, input data remains and button to ReConnect also remains,
so silly click on it will cause all data to load from your another server

[[https://github.com/anikitenko/mini-sftp-client/blob/master/doc-images/like-double-tab.gif|alt=like double tab]]
[[https://github.com/anikitenko/mini-sftp-client/blob/master/doc-images/like-double-tab-local.gif|alt=like double tab local]]

##### Notes:
* "Like double tab" works on Windows, OS X and Linux OS

##### Downloading files and using search:

[[https://github.com/anikitenko/mini-sftp-client/blob/master/doc-images/download-search.gif|alt=download and search]]

##### Notes:
* Search works the same for remote files as for local
* Search will not search for files globally, it's only sorting files which are exist

##### Quick buttons
* Quick buttons for remote files:
* Go Back: every time you navigate, client will save paths and on click button will return you to previous path
* Go Home: button will navigate you to initial directory which was opened during connection
* Go UP: navigates you to parent directory
* Refresh: refresh current directory
* Quick buttons for local files:
* Go Back: every time you navigate, client will save paths and on click button will return you to previous path
* Go Home: button will navigate you to initial directory which was opened during connection
* Go UP: navigates you to parent directory
* Create New Directory: create new empty directory and navigate to it
* Refresh: refresh current directory
13 changes: 13 additions & 0 deletions app/controllers/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package controllers

import (
"github.com/revel/revel"
)

type App struct {
*revel.Controller
}

func (c App) Index() revel.Result {
return c.Render()
}
49 changes: 49 additions & 0 deletions app/controllers/connect_via_ssh.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package controllers

import (
"os/user"
"path/filepath"

"github.com/revel/revel"
logger "github.com/sirupsen/logrus"
)

func (c App) ConnectViaSSH() revel.Result {
var resultMessage []string
data := make(map[string]interface{})

if connection := c.EstablishSSHConnection(); connection != nil {
return connection
}

defer SSHsession.Close()
defer SSHclient.Close()

if currentUserPathBytes, err := SSHsession.Output("echo -n $PWD"); err == nil {
data["remote_path"] = string(currentUserPathBytes)
} else {
data["remote_path"] = ""
logger.Warnf("Unable to determine remote path: %v", err)
resultMessage = append(resultMessage, "Unable to determine remote path")
}

var homeDirectory string
if username, err := user.Current(); err != nil {
if currentAbsPath, err := filepath.Abs("./"); err == nil {
homeDirectory = currentAbsPath
} else {
homeDirectory = ""
}
} else {
homeDirectory = username.HomeDir
}

data["local_path"] = homeDirectory

data["errors"] = resultMessage

data["local_path_separator"] = string(filepath.Separator)

response := CompileJSONResult(true, "", data)
return c.RenderJSON(response)
}
46 changes: 46 additions & 0 deletions app/controllers/connection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package controllers

import (
"errors"
"os"
"os/user"
"path/filepath"

logger "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
)

func createSSHSession(ip, userName, password, port string) (*ssh.Client, *ssh.Session, error) {
username, _ := user.Current()
userIdRSAFile := username.HomeDir + string(filepath.Separator) + ".ssh" + string(filepath.Separator) + "id_rsa"
userIdDSAFile := username.HomeDir + string(filepath.Separator) + ".ssh" + string(filepath.Separator) + "id_dsa"

sshConfig := &ssh.ClientConfig{}
sshConfig.User = userName
sshConfig.Auth = []ssh.AuthMethod{}
if password != "" {
sshConfig.Auth = append(sshConfig.Auth, ssh.Password(password))
}
if _, err := os.Stat(userIdRSAFile); err == nil {
sshConfig.Auth = append(sshConfig.Auth, PublicKeyFile(userIdRSAFile))
}
if _, err := os.Stat(userIdDSAFile); err == nil {
sshConfig.Auth = append(sshConfig.Auth, PublicKeyFile(userIdDSAFile))
}

sshConfig.HostKeyCallback = ssh.InsecureIgnoreHostKey()

client, err := ssh.Dial("tcp", ip+":"+port, sshConfig)
if err != nil {
logger.Warn(err.Error())
return nil, nil, errors.New("cannot dial")
}

session, err := client.NewSession()
if err != nil {
logger.Warn(err.Error())
return nil, nil, errors.New("unable to create session")
}

return client, session, nil
}
151 changes: 151 additions & 0 deletions app/controllers/download.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package controllers

import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"time"

"github.com/revel/revel"
logger "github.com/sirupsen/logrus"
)

func (c App) Download() revel.Result {
sourcePath := c.Params.Get("source_path")
localPath := c.Params.Get("local_path")
isDir := c.Params.Get("is_dir")
fileNamePost := c.Params.Get("file_name")
fileName := filepath.Base(fileNamePost)

if connection := c.EstablishSSHConnection(); connection != nil {
return connection
}

defer SSHclient.Close()

if dirOrNot, err := strconv.ParseBool(isDir); err != nil {
logger.Warnf("We don't understand if it's a file or directory: %v", err)
response := CompileJSONResult(false, "We don't understand if it's a file or directory")
return c.RenderJSON(response)
} else {
if dirOrNot {
stdoutNewPipe, err := SSHsession.StdoutPipe()
if err != nil {
logger.Warnf("Cannot create a pipe to download folder: %v", err)
response := CompileJSONResult(false, "Cannot create a pipe to download folder")
return c.RenderJSON(response)
}
tempArchiveName := fmt.Sprintf("%sdownload_%v.tar.gz", localPath+string(filepath.Separator), time.Now().UnixNano())
tempArchiveFile, err := os.OpenFile(tempArchiveName, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
logger.Warnf("Cannot open temp archive to write to: %v", err)
response := CompileJSONResult(false, "Cannot open temp archive to write to")
return c.RenderJSON(response)
}
if err := SSHsession.Start("tar cz --directory='" + sourcePath + "' '" + fileName + "'"); err != nil {
logger.Warnf("Problem with running command via SSH: %v", err)
response := CompileJSONResult(false, "Problem with running command via SSH")
return c.RenderJSON(response)
}
_, err = io.Copy(tempArchiveFile, stdoutNewPipe)
if err != nil {
logger.Warnf("Problem with copying archive via SSH: %v", err)
response := CompileJSONResult(false, "Problem with copying archive via SSH")
return c.RenderJSON(response)
}

//if err := SSHsession.Wait(); err != nil {
// logger.Warnf("Something is wrong while waiting for command to complete: %v", err)
// response := CompileJSONResult(false, "Something is wrong while waiting for command to complete")
// return c.RenderJSON(response)
//}

SSHsession.Close()
tempArchiveFile.Close()

archiveOpen, err := os.Open(tempArchiveName)
if err != nil {
if err := os.Remove(tempArchiveName); err != nil {
logger.Warnf("Unable to remove temp archive: %v", err)
}
logger.Warnf("Problem with opening temp archive: %v", err)
response := CompileJSONResult(false, "Problem with opening temp archive")
return c.RenderJSON(response)
}
defer func() {
archiveOpen.Close()
if err := os.Remove(tempArchiveName); err != nil {
logger.Warnf("Unable to remove temp archive: %v", err)
}
}()

gzipOpen, err := gzip.NewReader(archiveOpen)
if err != nil {
logger.Warnf("Problem with creating stream from temp archive: %v", err)
response := CompileJSONResult(false, "Problem with creating stream from temp archive")
return c.RenderJSON(response)
}

tarReader := tar.NewReader(gzipOpen)

for {
header, err := tarReader.Next()

if err == io.EOF {
break
}
if err != nil {
logger.Warnf("Problem with reading temp archive: %v", err)
response := CompileJSONResult(false, "Problem with reading temp archive")
return c.RenderJSON(response)
}

switch header.Typeflag {
case tar.TypeDir:
if err := os.Mkdir(localPath+string(filepath.Separator)+header.Name, 0755); err != nil {
logger.Warnf("Cannot create a directory from archive: %v", err)
response := CompileJSONResult(false, "Cannot create a directory from archive")
return c.RenderJSON(response)
}
case tar.TypeReg:
outFile, err := os.Create(localPath + string(filepath.Separator) + header.Name)
if err != nil {
logger.Warnf("Cannot create a file from archive: %v", err)
response := CompileJSONResult(false, "Cannot create a file from archive")
return c.RenderJSON(response)
}

if _, err := io.Copy(outFile, tarReader); err != nil {
logger.Warnf("Failed to write to a file from archive: %v", err)
response := CompileJSONResult(false, "Failed to write to a file from archive")
return c.RenderJSON(response)
}
outFile.Close()
default:
logger.Warnf("General archive problem: uknown type: %s in %s", header.Typeflag, header.Name)
response := CompileJSONResult(false, "General archive problem, Refer to logs :(")
return c.RenderJSON(response)
}
}
} else {
remoteFileContent, err := SSHsession.Output("cat '" + fileNamePost + "'")
if err != nil {
logger.Warnf("Problem with getting file content: %v", err)
response := CompileJSONResult(false, "Problem with getting file content")
return c.RenderJSON(response)
}
if err := ioutil.WriteFile(localPath+string(filepath.Separator)+fileName, remoteFileContent, 644); err != nil {
logger.Warnf("Problem with writing file content: %v", err)
response := CompileJSONResult(false, "Problem with writing file content")
return c.RenderJSON(response)
}
}
}
response := CompileJSONResult(true, "")
return c.RenderJSON(response)
}
Loading

0 comments on commit 9914b3e

Please sign in to comment.