Preparing for github file persistence

This commit is contained in:
Cássio Zen 2016-07-19 17:11:22 -03:00
parent 6f0f13ad40
commit 18ad041d96
13 changed files with 181 additions and 44 deletions

View File

@ -73,6 +73,7 @@
"draft-js-export-markdown": "^0.2.0",
"draft-js-import-markdown": "^0.1.6",
"fuzzy": "^0.1.1",
"js-base64": "^2.1.9",
"json-loader": "^0.5.4",
"localforage": "^1.4.2",
"lodash": "^4.13.1"

View File

@ -11,7 +11,7 @@ export const ENTRIES_REQUEST = 'ENTRIES_REQUEST';
export const ENTRIES_SUCCESS = 'ENTRIES_SUCCESS';
export const ENTRIES_FAILURE = 'ENTRIES_FAILURE';
export const DRAFT_CREATE = 'DRAFT_CREATE';
export const DRAFT_CREATE_FROM_ENTRY = 'DRAFT_CREATE_FROM_ENTRY';
export const DRAFT_DISCARD = 'DRAFT_DISCARD';
export const DRAFT_CHANGE = 'DRAFT_CHANGE';
@ -104,9 +104,9 @@ function entryPersistFail(collection, entry, error) {
/*
* Exported simple Action Creators
*/
export function createDraft(entry) {
export function createDraftFromEntry(entry) {
return {
type: DRAFT_CREATE,
type: DRAFT_CREATE_FROM_ENTRY,
payload: entry
};
}
@ -152,12 +152,12 @@ export function loadEntries(collection) {
};
}
export function persist(collection, entry, mediaFiles) {
export function persistEntry(collection, entry, mediaFiles) {
return (dispatch, getState) => {
const state = getState();
const backend = currentBackend(state.config);
dispatch(entryPersisting(collection, entry));
backend.persist(collection, entry, mediaFiles).then(
backend.persistEntry(collection, entry, mediaFiles).then(
({persistedEntry, persistedMediaFiles}) => {
dispatch(entryPersisted(persistedEntry, persistedMediaFiles));
},

View File

@ -5,6 +5,6 @@ export function addMedia(mediaProxy) {
return { type: ADD_MEDIA, payload: mediaProxy };
}
export function removeMedia(uri) {
return { type: REMOVE_MEDIA, payload: uri };
export function removeMedia(path) {
return { type: REMOVE_MEDIA, payload: path };
}

View File

@ -67,14 +67,21 @@ class Backend {
};
}
persist(collection, entryDraft) {
persistEntry(collection, entryDraft) {
const entryData = entryDraft.getIn(['entry', 'data']).toObject();
const entryObj = {
path: entryDraft.getIn(['entry', 'path']),
slug: entryDraft.getIn(['entry', 'slug']),
raw: this.entryToRaw(collection, entryData)
};
return this.implementation.persist(collection, entryObj, entryDraft.get('mediaFiles').toJS()).then(
const commitMessage = (entryDraft.getIn(['entry', 'newRecord']) ? 'Created ' : 'Updated ') +
collection.get('label') + ' “' +
entryDraft.getIn(['entry', 'data', 'title']) + '”';
return this.implementation.persistEntry(collection, entryObj, entryDraft.get('mediaFiles').toJS(), { commitMessage })
.then(
(response) => ({
persistedEntry: this.entryWithFormat(collection)(response.persistedEntry),
persistedMediaFiles:response.persistedMediaFiles

View File

@ -1,5 +1,7 @@
import LocalForage from 'localforage';
import MediaProxy from '../../valueObjects/MediaProxy';
import AuthenticationPage from './AuthenticationPage';
import { Base64 } from 'js-base64';
const API_ROOT = 'https://api.github.com';
@ -40,6 +42,43 @@ class API {
});
}
persistFiles(collection, entry, mediaFiles, options) {
let filename, part, parts, subtree;
const fileTree = {};
const files = [];
mediaFiles.concat(entry).forEach((file) => {
if (file.uploaded) { return; }
files.push(this.uploadBlob(file));
parts = file.path.split('/').filter((part) => part);
filename = parts.pop();
subtree = fileTree;
while (part = parts.shift()) {
subtree[part] = subtree[part] || {};
subtree = subtree[part];
}
subtree[filename] = file;
file.file = true;
});
return Promise.all(files)
.then(() => this.getBranch())
.then((branchData) => {
return this.updateTree(branchData.commit.sha, '/', fileTree);
})
.then((changeTree) => {
return this.request(`${this.repoURL}/git/commits`, {
type: 'POST',
data: JSON.stringify({ message: options.message, tree: changeTree.sha, parents: [changeTree.parentSha] })
});
}).then((response) => {
return this.request(`${this.repoURL}/git/refs/heads/${this.branch}`, {
type: 'PATCH',
data: JSON.stringify({ sha: response.sha })
});
});
}
requestHeaders(headers = {}) {
return {
Authorization: `token ${this.token}`,
@ -68,6 +107,78 @@ class API {
return response.text();
});
}
getBranch() {
return this.request(`${this.repoURL}/branches/${this.branch}`);
}
getTree(sha) {
return sha ? this.request(`${this.repoURL}/git/trees/${sha}`) : Promise.resolve({ tree: [] });
}
toBase64(str) {
return Promise.resolve(
Base64.encode(str)
);
}
uploadBlob(item) {
const content = item instanceof MediaProxy ? item.toBase64() : this.toBase64(item.raw);
return content.then((contentBase64) => {
return this.request(`${this.repoURL}/git/blobs`, {
method: 'POST',
body: JSON.stringify({
content: contentBase64,
encoding: 'base64'
})
}).then((response) => {
item.sha = response.sha;
item.uploaded = true;
return item;
});
});
}
updateTree(sha, path, fileTree) {
return this.getTree(sha)
.then((tree) => {
var obj, filename, fileOrDir;
var updates = [];
var added = {};
for (var i = 0, len = tree.tree.length; i < len; i++) {
obj = tree.tree[i];
if (fileOrDir = fileTree[obj.path]) {
added[obj.path] = true;
if (fileOrDir.file) {
updates.push({ path: obj.path, mode: obj.mode, type: obj.type, sha: fileOrDir.sha });
} else {
updates.push(this.updateTree(obj.sha, obj.path, fileOrDir));
}
}
}
for (filename in fileTree) {
fileOrDir = fileTree[filename];
if (added[filename]) { continue; }
updates.push(
fileOrDir.file ?
{ path: filename, mode: '100644', type: 'blob', sha: fileOrDir.sha } :
this.updateTree(null, filename, fileOrDir)
);
}
return Promise.all(updates)
.then((updates) => {
return this.request(`${this.repoURL}/git/trees`, {
type: 'POST',
data: JSON.stringify({ base_tree: sha, tree: updates })
});
}).then((response) => {
return { path: path, mode: '040000', type: 'tree', sha: response.sha, parentSha: sha };
});
});
}
}
export default class GitHub {
@ -115,4 +226,8 @@ export default class GitHub {
response.entries.filter((entry) => entry.slug === slug)[0]
));
}
persistEntry(collection, entry, mediaFiles = []) {
return this.api.persistFiles(collection, entry, mediaFiles);
}
}

View File

@ -48,10 +48,10 @@ export default class TestRepo {
));
}
persist(collection, entry, mediaFiles = []) {
const folder = collection.get('folder');
persistEntry(collection, entry, mediaFiles = []) {
const folder = entry.path.substring(0, entry.path.lastIndexOf('/'));
const fileName = entry.path.substring(entry.path.lastIndexOf('/') + 1);
window.repoFiles[folder][fileName]['content'] = entry.raw;
return Promise.resolve({persistedEntry:entry, persistedMediaFiles:[]});
return Promise.resolve({ persistedEntry:entry, persistedMediaFiles:[] });
}
}

View File

@ -53,7 +53,7 @@ export default class ImageControl extends React.Component {
if (file) {
const mediaProxy = new MediaProxy(file.name, file);
this.props.onAddMedia(mediaProxy);
this.props.onChange(mediaProxy.uri);
this.props.onChange(mediaProxy.path);
} else {
this.props.onChange(null);
}
@ -63,7 +63,7 @@ export default class ImageControl extends React.Component {
renderImageName() {
if (!this.props.value) return null;
if (this.value instanceof MediaProxy) {
return truncateMiddle(this.props.value.uri, MAX_DISPLAY_LENGTH);
return truncateMiddle(this.props.value.path, MAX_DISPLAY_LENGTH);
} else {
return truncateMiddle(this.props.value, MAX_DISPLAY_LENGTH);
}

View File

@ -3,10 +3,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import {
loadEntry,
createDraft,
createDraftFromEntry,
discardDraft,
changeDraft,
persist
persistEntry
} from '../actions/entries';
import { addMedia, removeMedia } from '../actions/media';
import { selectEntry, getMedia } from '../reducers';
@ -16,18 +16,18 @@ class EntryPage extends React.Component {
constructor(props) {
super(props);
this.props.loadEntry(props.collection, props.slug);
this.handlePersist = this.handlePersist.bind(this);
this.handlePersistEntry = this.handlePersistEntry.bind(this);
}
componentDidMount() {
if (this.props.entry) {
this.props.createDraft(this.props.entry);
this.props.createDraftFromEntry(this.props.entry);
}
}
componentWillReceiveProps(nextProps) {
if (this.props.entry !== nextProps.entry && !nextProps.entry.get('isFetching')) {
this.props.createDraft(nextProps.entry);
this.props.createDraftFromEntry(nextProps.entry);
}
}
@ -35,8 +35,8 @@ class EntryPage extends React.Component {
this.props.discardDraft();
}
handlePersist() {
this.props.persist(this.props.collection, this.props.entryDraft);
handlePersistEntry() {
this.props.persistEntry(this.props.collection, this.props.entryDraft);
}
render() {
@ -56,7 +56,7 @@ class EntryPage extends React.Component {
onChange={changeDraft}
onAddMedia={addMedia}
onRemoveMedia={removeMedia}
onPersist={this.handlePersist}
onPersist={this.handlePersistEntry}
/>
);
}
@ -67,12 +67,12 @@ EntryPage.propTypes = {
boundGetMedia: PropTypes.func.isRequired,
changeDraft: PropTypes.func.isRequired,
collection: ImmutablePropTypes.map.isRequired,
createDraft: PropTypes.func.isRequired,
createDraftFromEntry: PropTypes.func.isRequired,
discardDraft: PropTypes.func.isRequired,
entry: ImmutablePropTypes.map.isRequired,
entryDraft: ImmutablePropTypes.map.isRequired,
loadEntry: PropTypes.func.isRequired,
persist: PropTypes.func.isRequired,
persistEntry: PropTypes.func.isRequired,
removeMedia: PropTypes.func.isRequired,
slug: PropTypes.string.isRequired,
};
@ -93,8 +93,8 @@ export default connect(
addMedia,
removeMedia,
loadEntry,
createDraft,
createDraftFromEntry,
discardDraft,
persist
persistEntry
}
)(EntryPage);

View File

@ -19,7 +19,7 @@ const ImageType = new yaml.Type('image', {
kind: 'scalar',
instanceOf: MediaProxy,
represent: function(value) {
return `${value.uri}`;
return `${value.path}`;
},
resolve: function(value) {
if (value === null) return false;

View File

@ -1,12 +1,12 @@
import { Map, List } from 'immutable';
import { DRAFT_CREATE, DRAFT_DISCARD, DRAFT_CHANGE } from '../actions/entries';
import { DRAFT_CREATE_FROM_ENTRY, DRAFT_DISCARD, DRAFT_CHANGE } from '../actions/entries';
import { ADD_MEDIA, REMOVE_MEDIA } from '../actions/media';
const initialState = Map({ entry: Map(), mediaFiles: List() });
const entryDraft = (state = Map(), action) => {
switch (action.type) {
case DRAFT_CREATE:
case DRAFT_CREATE_FROM_ENTRY:
if (!action.payload) {
// New entry
return initialState;
@ -14,6 +14,7 @@ const entryDraft = (state = Map(), action) => {
// Existing Entry
return state.withMutations((state) => {
state.set('entry', action.payload);
state.setIn(['entry', 'newRecord'], false);
state.set('mediaFiles', List());
});
case DRAFT_DISCARD:
@ -22,9 +23,9 @@ const entryDraft = (state = Map(), action) => {
return state.set('entry', action.payload);
case ADD_MEDIA:
return state.update('mediaFiles', (list) => list.push(action.payload.uri));
return state.update('mediaFiles', (list) => list.push(action.payload.path));
case REMOVE_MEDIA:
return state.update('mediaFiles', (list) => list.filterNot((uri) => uri === action.payload));
return state.update('mediaFiles', (list) => list.filterNot((path) => path === action.payload));
default:
return state;

View File

@ -23,5 +23,5 @@ export const selectEntry = (state, collection, slug) =>
export const selectEntries = (state, collection) =>
fromEntries.selectEntries(state.entries, collection);
export const getMedia = (state, uri) =>
fromMedias.getMedia(state.medias, uri);
export const getMedia = (state, path) =>
fromMedias.getMedia(state.medias, path);

View File

@ -6,12 +6,12 @@ import MediaProxy from '../valueObjects/MediaProxy';
const medias = (state = Map(), action) => {
switch (action.type) {
case ADD_MEDIA:
return state.set(action.payload.uri, action.payload);
return state.set(action.payload.path, action.payload);
case REMOVE_MEDIA:
return state.delete(action.payload);
case ENTRY_PERSIST_SUCCESS:
return state.map((media, uri) => {
if (action.payload.persistedMediaFiles.indexOf(uri) > -1) media.uploaded = true;
return state.map((media, path) => {
if (action.payload.persistedMediaFiles.indexOf(path) > -1) media.uploaded = true;
return media;
});
@ -22,10 +22,10 @@ const medias = (state = Map(), action) => {
export default medias;
export const getMedia = (state, uri) => {
if (state.has(uri)) {
return state.get(uri);
export const getMedia = (state, path) => {
if (state.has(path)) {
return state.get(path);
} else {
return new MediaProxy(uri, null, true);
return new MediaProxy(path, null, true);
}
};

View File

@ -7,8 +7,21 @@ export default function MediaProxy(value, file, uploaded = false) {
this.value = value;
this.file = file;
this.uploaded = uploaded;
this.uri = config.media_folder && !uploaded ? config.media_folder + '/' + value : value;
this.toString = function() {
return this.uploaded ? this.uri : window.URL.createObjectURL(this.file, { oneTimeOnly: true });
};
this.sha = null;
this.path = config.media_folder && !uploaded ? config.media_folder + '/' + value : value;
}
MediaProxy.prototype.toString = function() {
return this.uploaded ? this.path : window.URL.createObjectURL(this.file, { oneTimeOnly: true });
};
MediaProxy.prototype.toBase64 = function() {
return new Promise((resolve, reject) => {
const fr = new FileReader();
fr.onload = (readerEvt) => {
const binaryString = readerEvt.target.result;
resolve(btoa(binaryString));
};
fr.readAsDataURL(this.file);
});
};