Skip to content
This repository has been archived by the owner on Nov 12, 2019. It is now read-only.

Commit

Permalink
feat(ftp): implement ftp browser, most things are functioning except …
Browse files Browse the repository at this point in the history
…archiving and some actions of multiple files
  • Loading branch information
Mahdi Dibaiee committed Oct 29, 2015
1 parent 9aa5bcf commit 2eaf2ac
Show file tree
Hide file tree
Showing 11 changed files with 1,437 additions and 382 deletions.
1,463 changes: 1,108 additions & 355 deletions build/main.js

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions build/manifest.webapp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
"device-storage:music": {
"access": "readwrite",
"description": "We need access to your files in order to give you the functionality of listing, reading, opening and writing files in your storage"
},
"tcp-socket": {
"description": "FTP Browser: Used to connect to FTP servers"
}
},
"installs_allowed_from": [
Expand Down
14 changes: 14 additions & 0 deletions src/js/api/auto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as ftp from './ftp';
import * as files from './files';

['getFile', 'children', 'isDirectory', 'readFile', 'writeFile',
'createFile', 'createDirectory', 'remove', 'move', 'copy'].forEach(method => {
exports[method] = (...args) => {
return window.ftpMode ? ftp[method](...args) : files[method](...args);
}
});

let CACHE = files.CACHE;
let FTP_CACHE = ftp.FTP_CACHE;

export { CACHE, FTP_CACHE };
18 changes: 16 additions & 2 deletions src/js/api/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,14 @@ export async function getFile(dir = '/') {

if (dir === '/' || !dir) return parent;

return await parent.get(normalize(dir));
let file = await parent.get(normalize(dir));

Object.defineProperty(file, 'type', {
value: type(file),
enumerable: true
});

return file;
}

export async function children(dir, gatherInfo) {
Expand All @@ -46,9 +53,16 @@ export async function children(dir, gatherInfo) {
}
let childs = await parent.getFilesAndDirectories();

for (let child of childs) {
Object.defineProperty(child, 'type', {
value: type(child),
enumerable: true
});
}

if (gatherInfo && !window.needsShim) {
for (let child of childs) {
if (type(child) === 'Directory') {
if (child.type === 'Directory') {
let subchildren;
try {
subchildren = await shimDirectory(child).getFilesAndDirectories();
Expand Down
267 changes: 267 additions & 0 deletions src/js/api/ftp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
import { refresh } from 'actions/files-view';
import { bind } from 'store';
import Eventconnection from 'events';
import { humanSize, reportError, normalize, type } from 'utils';

export let FTP_CACHE = {};
let socket;
let connection = new Eventconnection();
connection.setMaxListeners(99);
let wd = '';
let currentRequest;
let queue = 0;

export async function connect(properties = {}) {
let { host, port, username, password } = properties;

let url = encodeURI(host);
socket = navigator.mozTCPSocket.open(url, port);

socket.ondata = e => {
console.log('<', e.data);
connection.emit('data', e.data);
}

socket.onerror = e => {
connection.emit('error', e.data);
}

socket.onclose = e => {
connection.emit('close', e.data);
}

return new Promise((resolve, reject) => {
socket.onopen = () => {
send(`USER ${username}`);
send(`PASS ${password}`);
resolve(socket);

window.ftpMode = true;
}

socket.onerror = reject;
socket.onclose = reject;
});
}

export async function disconnect() {
socket.close();
window.ftpMode = false;
}

export function listen(ev, fn) {
socket.listen(ev, fn);
}

export function send(command, ...args) {
args = args.filter(arg => arg);
let cmd = command + (args.length ? ' ' : '') + args.join(' ');

console.log('>', cmd);
socket.send(cmd + '\n');
}

export async function cwd(dir = '') {
send('CWD', dir);
wd = dir;
}

const PWD_REGEX = /257 "(.*)"/;
export async function pwd() {
return new Promise((resolve, reject) => {
connection.on('data', function listener(data) {
if (data.indexOf('current directory') === -1) return;
let dir = data.match(PWD_REGEX)[1];
resolve(normalize(dir));

connection.removeListener('data', listener);
});
send('PWD');
});
}

export async function pasv() {
return new Promise((resolve, reject) => {
connection.on('data', function listener(data) {
if (data.indexOf('Passive') === -1) return;

// format: |||port|
let port = parseInt(data.match(/\|{3}(\d+)\|/)[1]);

connection.removeListener('data', listener);

return resolve(port);
});

send('EPSV');
});
}

const LIST_EXTRACTOR = /(.*?)\s+(\d+)\s+(\w+)\s+(\w+)\s+(\w+)\s+(\w+)\s+(\d+)\s+(\d+:?\d+)+\s+(.*)/;
export async function list(dir = '') {
return pasv().then(port => {
return secondary({ host: socket.host, port }).then(({data}) => {
send('LIST', dir);

return data.then(items => {
return items.split('\n').map(item => {
if (item.indexOf('total') > -1 || !item) return;

let match = item.match(LIST_EXTRACTOR);

return {
path: normalize(wd) + '/',
type: match[1][0] === 'd' ? 'Directory' : 'File',
permissions: match[1].slice(1),
links: +match[2],
owner: match[3],
group: match[4],
size: +match[5],
lastModification: {
month: match[6],
day: match[7],
time: match[8]
},
name: match[9]
}
}).filter(item => item);
}, reportError)
});
});
}

export async function namelist(dir = '') {
return pasv().then(port => {
return secondary({ host: socket.host, port }).then(({data}) => {
send('NLST', dir);

return data.then(names => names.split('\n'), reportError);
});
})
}

export async function secondary(properties = {}) {
let { host, port } = properties;

let url = encodeURI(host);

return new Promise((resolve, reject) => {
let alt = navigator.mozTCPSocket.open(url, port);

alt.onopen = () => {
let data = new Promise((resolve, reject) => {
alt.ondata = e => {
resolve(e.data);
}
alt.onerror = e => {
reject(e.data);
}
alt.onclose = e => {
resolve('');
}
});
resolve({data});
}
})
}

export async function secondaryWrite(properties = {}, content) {
let { host, port } = properties;

let url = encodeURI(host);

return new Promise((resolve, reject) => {
let alt = navigator.mozTCPSocket.open(url, port);

alt.onopen = () => {
alt.send(content);

setImmediate(() => {
alt.close();
})
}

alt.onclose = () => {
resolve();
}
})
}

export async function children(dir = '', gatherInfo) {
dir = normalize(dir);
if (FTP_CACHE[dir]) return FTP_CACHE[dir];

let childs = gatherInfo ? await list(dir) : await namelist();

FTP_CACHE[dir] = childs;

return childs;
}

export async function getFile(path = '') {
path = normalize(path);

let ls = await list(path);

return ls[0];
}

export async function isDirectory(path = '') {
return (await getFile(path)).type === 'Directory';
}

export async function readFile(path = '') {
path = normalize(path);

return pasv().then(port => {
return secondary({ host: socket.host, port }).then(({data}) => {
send('RETR', path);

return data;
});
}).catch(reportError);
}

export async function writeFile(path = '', content) {
path = normalize(path);

return pasv().then(port => {
send('STOR', path);
return secondaryWrite({ host: socket.host, port }, content).then(() => {
})
}).catch(reportError);
}

export async function createFile(path = '') {
return writeFile(path, '');
}

export async function createDirectory(path = '') {
path = normalize(path);

send('MKD', path);
}

export async function remove(path = '') {
path = normalize(path);

send('RMD', path);
send('DELE', path);
}

export async function move(path = '', newPath = '') {
path = normalize(path);
newPath = normalize(newPath);

send('RNFR', path);
send('RNTO', newPath);
}

export async function copy(path = '', newPath = '') {
path = normalize(path);
newPath = normalize(newPath);

let content = await readFile(path);

return writeFile(newPath, content);
}
9 changes: 1 addition & 8 deletions src/js/components/file-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default class FileList extends Component {

let els = files.map((file, index) => {
let selected = activeFile.indexOf(file) > -1;
if (type(file) === 'File') {
if (file.type === 'File') {
return <File selectView={selectView} selected={selected} key={index} index={index} name={file.name} size={file.size} type={file.type} />;
} else {
return <Directory selectView={selectView} selected={selected} key={index} index={index} name={file.name} children={file.children} type={file.type} />
Expand Down Expand Up @@ -59,10 +59,3 @@ function props(state) {
view: state.get('settings').view || 'list'
}
}

async function getFiles(dir) {
let storage = navigator.getDeviceStorage('sdcard');
let root = await storage.get(dir);

return await root.getFilesAndDirectories();
}
11 changes: 11 additions & 0 deletions src/js/main.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import React from 'react';
import * as ftp from 'api/ftp';
import Root from 'components/root';
import store from 'store';
import { Provider } from 'react-redux';
import './activities';

ftp.connect({
host: '192.168.1.76',
port: 21,
username: 'mahdi',
password: 'heater0!'
}).then(socket => {
window.socket = socket;
window.ftp = ftp;
}, console.error)

let wrapper = document.getElementById('wrapper');
React.render(<Provider store={store}>{() => <Root />}</Provider>, wrapper);
3 changes: 2 additions & 1 deletion src/js/reducers/cwd.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CHANGE_DIRECTORY, REFRESH, SETTINGS } from 'actions/types';
import { children, CACHE } from 'api/files';
import { children, CACHE, FTP_CACHE } from 'api/auto';
import store from 'store';
import { reportError, normalize } from 'utils';
import { listFiles } from 'actions/files-view';
Expand All @@ -13,6 +13,7 @@ export default function(state = '', action) {

if (action.type === REFRESH) {
CACHE[state] = null;
FTP_CACHE[state] = null;
}

if (action.type === REFRESH || action.type === SETTINGS) {
Expand Down
Loading

0 comments on commit 2eaf2ac

Please sign in to comment.