feat(ftp): implement ftp browser, most things are functioning except archiving and some actions of multiple files

This commit is contained in:
Mahdi Dibaiee 2015-10-29 11:31:28 +03:30
parent 9aa5bcf384
commit 2eaf2ac1f0
11 changed files with 1437 additions and 382 deletions

File diff suppressed because it is too large Load Diff

View File

@ -30,6 +30,9 @@
"device-storage:music": { "device-storage:music": {
"access": "readwrite", "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" "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": [ "installs_allowed_from": [

14
src/js/api/auto.js Normal file
View File

@ -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 };

View File

@ -34,7 +34,14 @@ export async function getFile(dir = '/') {
if (dir === '/' || !dir) return parent; 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) { export async function children(dir, gatherInfo) {
@ -46,9 +53,16 @@ export async function children(dir, gatherInfo) {
} }
let childs = await parent.getFilesAndDirectories(); let childs = await parent.getFilesAndDirectories();
for (let child of childs) {
Object.defineProperty(child, 'type', {
value: type(child),
enumerable: true
});
}
if (gatherInfo && !window.needsShim) { if (gatherInfo && !window.needsShim) {
for (let child of childs) { for (let child of childs) {
if (type(child) === 'Directory') { if (child.type === 'Directory') {
let subchildren; let subchildren;
try { try {
subchildren = await shimDirectory(child).getFilesAndDirectories(); subchildren = await shimDirectory(child).getFilesAndDirectories();

267
src/js/api/ftp.js Normal file
View File

@ -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);
}

View File

@ -20,7 +20,7 @@ export default class FileList extends Component {
let els = files.map((file, index) => { let els = files.map((file, index) => {
let selected = activeFile.indexOf(file) > -1; 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} />; return <File selectView={selectView} selected={selected} key={index} index={index} name={file.name} size={file.size} type={file.type} />;
} else { } else {
return <Directory selectView={selectView} selected={selected} key={index} index={index} name={file.name} children={file.children} type={file.type} /> return <Directory selectView={selectView} selected={selected} key={index} index={index} name={file.name} children={file.children} type={file.type} />
@ -59,10 +59,3 @@ function props(state) {
view: state.get('settings').view || 'list' 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();
}

View File

@ -1,8 +1,19 @@
import React from 'react'; import React from 'react';
import * as ftp from 'api/ftp';
import Root from 'components/root'; import Root from 'components/root';
import store from 'store'; import store from 'store';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import './activities'; 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'); let wrapper = document.getElementById('wrapper');
React.render(<Provider store={store}>{() => <Root />}</Provider>, wrapper); React.render(<Provider store={store}>{() => <Root />}</Provider>, wrapper);

View File

@ -1,5 +1,5 @@
import { CHANGE_DIRECTORY, REFRESH, SETTINGS } from 'actions/types'; 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 store from 'store';
import { reportError, normalize } from 'utils'; import { reportError, normalize } from 'utils';
import { listFiles } from 'actions/files-view'; import { listFiles } from 'actions/files-view';
@ -13,6 +13,7 @@ export default function(state = '', action) {
if (action.type === REFRESH) { if (action.type === REFRESH) {
CACHE[state] = null; CACHE[state] = null;
FTP_CACHE[state] = null;
} }
if (action.type === REFRESH || action.type === SETTINGS) { if (action.type === REFRESH || action.type === SETTINGS) {

View File

@ -1,7 +1,7 @@
import { LIST_FILES, RENAME_FILE, DELETE_FILE, CREATE_FILE, MOVE_FILE, COPY_FILE, SEARCH, COMPRESS, DECOMPRESS } from 'actions/types'; import { LIST_FILES, RENAME_FILE, DELETE_FILE, CREATE_FILE, MOVE_FILE, COPY_FILE, SEARCH, COMPRESS, DECOMPRESS } from 'actions/types';
import zip from 'jszip'; import zip from 'jszip';
import { refresh } from 'actions/files-view'; import { refresh } from 'actions/files-view';
import { move, remove, sdcard, createFile, readFile, writeFile, createDirectory, getFile, copy, children } from 'api/files'; import * as auto from 'api/auto';
import { show } from 'actions/dialog'; import { show } from 'actions/dialog';
import store, { bind } from 'store'; import store, { bind } from 'store';
import { reportError, type, normalize } from 'utils'; import { reportError, type, normalize } from 'utils';
@ -39,7 +39,7 @@ export default function(state = [], action) {
} }
if (action.type === CREATE_FILE) { if (action.type === CREATE_FILE) {
let fn = action.directory ? createDirectory : createFile; let fn = action.directory ? auto.createDirectory : auto.createFile;
fn(action.path).then(boundRefresh, reportError); fn(action.path).then(boundRefresh, reportError);
return state; return state;
@ -48,7 +48,7 @@ export default function(state = [], action) {
if (action.type === RENAME_FILE) { if (action.type === RENAME_FILE) {
let all = Promise.all(action.file.map(file => { let all = Promise.all(action.file.map(file => {
let cwd = store.getState().get('cwd'); let cwd = store.getState().get('cwd');
return move(file, cwd + '/' + action.name); return auto.move(file, cwd + '/' + action.name);
})); }));
all.then(boundRefresh, reportError); all.then(boundRefresh, reportError);
@ -57,7 +57,7 @@ export default function(state = [], action) {
if (action.type === MOVE_FILE) { if (action.type === MOVE_FILE) {
let all = Promise.all(action.file.map(file => { let all = Promise.all(action.file.map(file => {
return move(file, action.newPath + '/' + file.name); return auto.move(file, action.newPath + '/' + file.name);
})); }));
all.then(boundRefresh, reportError); all.then(boundRefresh, reportError);
@ -66,7 +66,7 @@ export default function(state = [], action) {
if (action.type === COPY_FILE) { if (action.type === COPY_FILE) {
let all = Promise.all(action.file.map(file => { let all = Promise.all(action.file.map(file => {
return copy(file, action.newPath + '/' + file.name); return auto.copy(file, action.newPath + '/' + file.name);
})); }));
all.then(boundRefresh, reportError); all.then(boundRefresh, reportError);
@ -76,7 +76,7 @@ export default function(state = [], action) {
if (action.type === DELETE_FILE) { if (action.type === DELETE_FILE) {
let all = Promise.all(action.file.map(file => { let all = Promise.all(action.file.map(file => {
let path = normalize((file.path || '') + file.name); let path = normalize((file.path || '') + file.name);
return remove(path, true); return auto.remove(path, true);
})) }))
all.then(boundRefresh, reportError); all.then(boundRefresh, reportError);
@ -95,14 +95,14 @@ export default function(state = [], action) {
if (!(file instanceof Blob)) { if (!(file instanceof Blob)) {
let folder = archive.folder(file.name); let folder = archive.folder(file.name);
return children(path).then(files => { return auto.children(path).then(files => {
return Promise.all(files.map(child => { return Promise.all(files.map(child => {
return addFile(child); return addFile(child);
})); }));
}); });
} }
return readFile(path).then(content => { return auto.readFile(path).then(content => {
archive.file(archivePath, content); archive.file(archivePath, content);
}); });
})) }))
@ -114,7 +114,7 @@ export default function(state = [], action) {
let cwd = store.getState().get('cwd'); let cwd = store.getState().get('cwd');
let path = normalize(cwd + '/' + action.name); let path = normalize(cwd + '/' + action.name);
console.log(path); console.log(path);
return writeFile(path, blob); return auto.writeFile(path, blob);
}).then(boundRefresh).catch(reportError); }).then(boundRefresh).catch(reportError);
return state; return state;
@ -123,7 +123,7 @@ export default function(state = [], action) {
if (action.type === DECOMPRESS) { if (action.type === DECOMPRESS) {
let file = action.file[0]; let file = action.file[0];
let path = normalize((file.path || '') + file.name); let path = normalize((file.path || '') + file.name);
readFile(path).then(content => { auto.readFile(path).then(content => {
let archive = new zip(content); let archive = new zip(content);
let files = Object.keys(archive.files); let files = Object.keys(archive.files);
@ -134,7 +134,7 @@ export default function(state = [], action) {
let cwd = store.getState().get('cwd'); let cwd = store.getState().get('cwd');
let filePath = normalize(cwd + '/' + name); let filePath = normalize(cwd + '/' + name);
return writeFile(filePath, blob); return auto.writeFile(filePath, blob);
})); }));
all.then(boundRefresh, reportError); all.then(boundRefresh, reportError);
@ -143,7 +143,3 @@ export default function(state = [], action) {
return state; return state;
} }
function mov(file, newPath) {
return
}

View File

@ -2,7 +2,7 @@ import { SEARCH, CHANGE_DIRECTORY, REFRESH } from 'actions/types';
import store from 'store'; import store from 'store';
import { reportError } from 'utils'; import { reportError } from 'utils';
import { listFiles } from 'actions/files-view'; import { listFiles } from 'actions/files-view';
import { children } from 'api/files'; import { children } from 'api/auto';
import { type, normalize } from 'utils'; import { type, normalize } from 'utils';
export default function(state = '', action) { export default function(state = '', action) {

View File

@ -30,6 +30,9 @@
"device-storage:music": { "device-storage:music": {
"access": "readwrite", "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" "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": [ "installs_allowed_from": [