allow claims to be updated and abandoned

This commit is contained in:
Travis Eden 2018-08-21 08:53:17 -04:00
parent 462528ef03
commit 34a66f1968
39 changed files with 779 additions and 161 deletions

View file

@ -14,6 +14,12 @@ export function clearFile () {
};
}
export function setUpdateTrue () {
return {
type: actions.SET_UPDATE_TRUE,
};
}
export function updateMetadata (name, value) {
return {
type: actions.METADATA_UPDATE,
@ -31,6 +37,13 @@ export function updateClaim (value) {
};
};
export function abandonClaim (data) {
return {
type: actions.ABANDON_CLAIM,
data,
};
};
export function setPublishInChannel (channel) {
return {
type: actions.SET_PUBLISH_IN_CHANNEL,

View file

@ -105,6 +105,13 @@ export function updateAssetViewsInList (id, claimId, claimViews) {
};
}
export function removeAsset (data) {
return {
type: actions.ASSET_REMOVE,
data,
};
}
// channel actions
export function addNewChannelToChannelList (id, name, shortId, longId, claimsData) {

View file

@ -42,3 +42,15 @@ export function getClaimViews (claimId) {
const url = `/api/claim/views/${claimId}`;
return Request(url);
}
export function doAbandonClaim (claimId) {
const params = {
method : 'POST',
body : JSON.stringify({claimId}),
headers: new Headers({
'Content-Type': 'application/json',
}),
credentials: 'include',
};
return Request('/api/claim/abandon', params);
}

View file

@ -10,6 +10,7 @@ import ContentPageWrapper from '@pages/ContentPageWrapper';
import FourOhFourPage from '@pages/FourOhFourPage';
import MultisitePage from '@pages/MultisitePage';
import PopularPage from '@pages/PopularPage';
import EditPage from '@pages/EditPage';
const App = () => {
return (
@ -21,6 +22,7 @@ const App = () => {
<Route exact path='/login' component={LoginPage} />
<Route exact path='/multisite' component={MultisitePage} />
<Route exact path='/popular' component={PopularPage} />
<Route exact path='/edit/:identifier/:claim' component={EditPage} />
<Route exact path='/:identifier/:claim' component={ContentPageWrapper} />
<Route exact path='/:claim' component={ContentPageWrapper} />
<Route component={FourOhFourPage} />

View file

@ -1,8 +1,8 @@
import {buffers, END, eventChannel} from 'redux-saga';
export const makePublishRequestChannel = (fd) => {
export const makePublishRequestChannel = (fd, isUpdate) => {
return eventChannel(emitter => {
const uri = '/api/claim/publish';
const uri = `/api/claim/${isUpdate ? 'update' : 'publish'}`;
const xhr = new XMLHttpRequest();
// add event listeners
const onLoadStart = () => {

View file

@ -10,3 +10,5 @@ export const TOGGLE_METADATA_INPUTS = 'TOGGLE_METADATA_INPUTS';
export const THUMBNAIL_NEW = 'THUMBNAIL_NEW';
export const PUBLISH_START = 'PUBLISH_START';
export const CLAIM_AVAILABILITY = 'CLAIM_AVAILABILITY';
export const SET_UPDATE_TRUE = 'SET_UPDATE_TRUE';
export const ABANDON_CLAIM = 'ABANDON_CLAIM';

View file

@ -3,3 +3,4 @@ export const LOADING = 'LOADING';
export const PUBLISHING = 'PUBLISHING';
export const SUCCESS = 'SUCCESS';
export const FAILED = 'FAILED';
export const ABANDONING = 'ABANDONING';

View file

@ -11,6 +11,8 @@ export const REQUEST_LIST_ADD = 'REQUEST_LIST_ADD';
// asset actions
export const ASSET_ADD = 'ASSET_ADD';
export const ASSET_VIEWS_UPDATE = 'ASSET_VIEWS_UPDATE';
export const ASSET_UPDATE_CLAIMDATA = 'ASSET_UPDATE_CLAIMDATA';
export const ASSET_REMOVE = 'ASSET_REMOVE';
// channel actions
export const CHANNEL_ADD = 'CHANNEL_ADD';

View file

@ -3,7 +3,8 @@ import View from './view';
import { fileRequested } from '../../actions/show';
import { selectAsset } from '../../selectors/show';
const mapStateToProps = ({ show }) => {
const mapStateToProps = (props) => {
const {show} = props;
// select error and status
const error = show.displayAsset.error;
const status = show.displayAsset.status;

View file

@ -3,14 +3,49 @@ import Row from '@components/Row';
import ProgressBar from '@components/ProgressBar';
import { LOCAL_CHECK, UNAVAILABLE, ERROR, AVAILABLE } from '../../constants/asset_display_states';
class AvailableContent extends React.Component {
render () {
const {contentType, sourceUrl, name, thumbnail} = this.props;
switch (contentType) {
case 'image/jpeg':
case 'image/jpg':
case 'image/png':
case 'image/gif':
return (
<img
className='asset-image'
src={sourceUrl}
alt={name}
/>
);
case 'video/mp4':
return (
<video
className='asset-video'
controls poster={thumbnail}
>
<source
src={sourceUrl}
/>
<p>Your browser does not support the <code>video</code> element.</p>
</video>
);
default:
return (
<p>Unsupported content type</p>
);
}
}
}
class AssetDisplay extends React.Component {
componentDidMount () {
const { asset: { claimData: { name, claimId } } } = this.props;
this.props.onFileRequest(name, claimId);
}
render () {
const { status, error, asset: { claimData: { name, claimId, contentType, fileExt, thumbnail } } } = this.props;
const sourceUrl = `/${claimId}/${name}.${fileExt}`;
const { status, error, asset: { name, claimData: { claimId, contentType, fileExt, thumbnail, outpoint } } } = this.props;
const sourceUrl = `/${claimId}/${name}.${fileExt}?${outpoint}`;
return (
<div className={'asset-display'}>
{(status === LOCAL_CHECK) &&
@ -36,37 +71,12 @@ class AssetDisplay extends React.Component {
</div>
}
{(status === AVAILABLE) &&
(() => {
switch (contentType) {
case 'image/jpeg':
case 'image/jpg':
case 'image/png':
case 'image/gif':
return (
<img
className='asset-image'
src={sourceUrl}
alt={name}
/>
);
case 'video/mp4':
return (
<video
className='asset-video'
controls poster={thumbnail}
>
<source
src={sourceUrl}
/>
<p>Your browser does not support the <code>video</code> element.</p>
</video>
);
default:
return (
<p>Unsupported content type</p>
);
}
})()
<AvailableContent
contentType={contentType}
sourceUrl={sourceUrl}
name={name}
thumbnail={thumbnail}
/>
}
</div>
);

View file

@ -2,7 +2,8 @@ import { connect } from 'react-redux';
import View from './view';
import { selectAsset } from '../../selectors/show';
const mapStateToProps = ({ show }) => {
const mapStateToProps = (props) => {
const {show} = props;
// select asset
const asset = selectAsset(show);
// return props

View file

@ -2,10 +2,14 @@ import { connect } from 'react-redux';
import View from './view';
import { selectAsset } from '../../selectors/show';
const mapStateToProps = ({ show }) => {
const { claimData: { title } } = selectAsset(show);
const mapStateToProps = (props) => {
const { claimData: { title, claimId, name, channelName } } = selectAsset(props.show);
const editable = Boolean(props.channel.loggedInChannel.name === channelName);
return {
title,
claimId,
name,
editable,
};
};

View file

@ -1,10 +1,14 @@
import React from 'react';
import { Link } from 'react-router-dom';
import Row from '@components/Row';
const AssetTitle = ({ title }) => {
const AssetTitle = ({ title, editable, claimId, name }) => {
return (
<Row>
<h3>{title}</h3>
<h3>
{title}
{editable && (<span> (<Link to={`/edit/${claimId}/${name}`}>edit</Link>)</span>)}
</h3>
</Row>
);
};

View file

@ -1,16 +1,29 @@
import { connect } from 'react-redux';
import { clearFile, startPublish } from '../../actions/publish';
import { clearFile, startPublish, abandonClaim } from '../../actions/publish';
import View from './view';
const mapStateToProps = ({ channel, publish }) => {
const mapStateToProps = ({ show, publish }) => {
// select request info
const requestId = show.request.id;
// select asset info
let asset;
const request = show.requestList[requestId] || null;
const assetList = show.assetList;
if (request && assetList) {
const assetKey = request.key; // note: just store this in the request
asset = assetList[assetKey] || null;
};
return {
file: publish.file,
file : publish.file,
isUpdate: publish.isUpdate,
asset,
};
};
const mapDispatchToProps = {
clearFile,
startPublish,
abandonClaim,
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -1,12 +1,15 @@
import React from 'react';
import { withRouter } from 'react-router-dom';
import {Link, withRouter} from 'react-router-dom';
import PublishUrlInput from '@containers/PublishUrlInput';
import PublishThumbnailInput from '@containers/PublishThumbnailInput';
import PublishMetadataInputs from '@containers/PublishMetadataInputs';
import ChannelSelect from '@containers/ChannelSelect';
import Row from '@components/Row';
import Label from '@components/Label';
import RowLabeled from '@components/RowLabeled';
import ButtonPrimaryJumbo from '@components/ButtonPrimaryJumbo';
import ButtonTertiary from '@components/ButtonTertiary';
import ButtonSecondary from '@components/ButtonSecondary';
import SpaceAround from '@components/SpaceAround';
import PublishFinePrint from '@components/PublishFinePrint';
@ -14,22 +17,62 @@ class PublishDetails extends React.Component {
constructor (props) {
super(props);
this.onPublishSubmit = this.onPublishSubmit.bind(this);
this.abandonClaim = this.abandonClaim.bind(this);
}
onPublishSubmit () {
this.props.startPublish(this.props.history);
}
abandonClaim () {
const {asset, history} = this.props;
if (asset) {
const {claimData} = asset;
this.props.abandonClaim({claimData, history});
}
}
render () {
const {file, isUpdate, asset} = this.props;
return (
<div>
<Row>
<PublishUrlInput />
</Row>
{isUpdate ? (asset && (
<React.Fragment>
<Row>
<RowLabeled
label={
<Label value={'Channel:'} />
}
content={
<span className='text'>
{asset.claimData.channelName}
</span>
}
/>
</Row>
<Row>
<RowLabeled
label={
<Label value={'Asset:'} />
}
content={
<span className='text'>
{asset.name}
</span>
}
/>
</Row>
</React.Fragment>
)) : (
<React.Fragment>
<Row>
<PublishUrlInput />
</Row>
<Row>
<ChannelSelect />
</Row>
<Row>
<ChannelSelect />
</Row>
</React.Fragment>
)}
{ this.props.file.type === 'video/mp4' && (
{ file && file.type === 'video/mp4' && (
<Row>
<PublishThumbnailInput />
</Row>
@ -46,6 +89,17 @@ class PublishDetails extends React.Component {
/>
</Row>
{isUpdate && (
<Row>
<SpaceAround>
<ButtonSecondary
value={'Abandon Claim'}
onClickHandler={this.abandonClaim}
/>
</SpaceAround>
</Row>
)}
<Row>
<SpaceAround>
<ButtonTertiary

View file

@ -71,6 +71,13 @@ class PublishStatus extends React.Component {
</Row>
</div>
}
{status === publishStates.ABANDONING &&
<div className={'status'}>
<Row>
<p>Your claim is being abandoned.</p>
</Row>
</div>
}
</div>
);
}

View file

@ -6,6 +6,7 @@ const mapStateToProps = ({ publish }) => {
disabled: publish.disabled,
file : publish.file,
status : publish.status.status,
isUpdate: publish.isUpdate,
};
};

View file

@ -6,18 +6,19 @@ import PublishDisabledMessage from '@containers/PublishDisabledMessage';
class PublishTool extends React.Component {
render () {
if (this.props.disabled) {
const {disabled, file, isUpdate, status} = this.props;
if (disabled) {
return (
<PublishDisabledMessage />
);
} else {
if (this.props.file) {
if (this.props.status) {
if (file || isUpdate) {
if (status) {
return (
<PublishStatus />
);
} else {
return <PublishPreview />;
return <PublishPreview isUpdate={isUpdate} />;
}
}
return <Dropzone />;

View file

@ -0,0 +1,31 @@
import { connect } from 'react-redux';
import { setUpdateTrue, updateMetadata, clearFile } from '../../actions/publish';
import { onHandleShowPageUri } from '../../actions/show';
import View from './view';
const mapStateToProps = (props) => {
const { show } = props;
// select request info
const requestId = show.request.id;
// select asset info
let asset;
const request = show.requestList[requestId] || null;
const assetList = show.assetList;
if (request && assetList) {
const assetKey = request.key; // note: just store this in the request
asset = assetList[assetKey] || null;
}
return {
asset,
myChannel: props.channel.loggedInChannel.name,
};
};
const mapDispatchToProps = {
updateMetadata,
onHandleShowPageUri,
setUpdateTrue,
clearFile,
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -0,0 +1,42 @@
import React from 'react';
import PageLayout from '@components/PageLayout';
import { Redirect } from 'react-router-dom';
import PublishTool from '@containers/PublishTool';
class EditPage extends React.Component {
componentDidMount () {
const {asset, match, onHandleShowPageUri, setUpdateTrue, updateMetadata} = this.props;
onHandleShowPageUri(match.params);
setUpdateTrue();
if (asset) {
['title', 'description', 'license', 'nsfw'].forEach(meta => updateMetadata(meta, asset.claimData[meta]));
}
}
componentWillUnmount () {
this.props.clearFile();
}
render () {
const { myChannel, asset } = this.props;
// redirect if user does not own this claim
if (
!myChannel || (
asset &&
asset.claimsData &&
asset.claimsData.channelName &&
asset.claimsData.channelName !== myChannel
)
) {
return (<Redirect to={'/'} />);
}
return (
<PageLayout
pageTitle={'Edit claim'}
pageUri={'edit'}
>
<PublishTool />
</PageLayout>
);
}
};
export default EditPage;

View file

@ -4,16 +4,19 @@ import PublishTool from '@containers/PublishTool';
import ContentPageWrapper from '@pages/ContentPageWrapper';
class HomePage extends React.Component {
componentDidMount () {
this.props.onHandleShowHomepage(this.props.match.params);
}
// componentDidMount () {
// this.props.onHandleShowHomepage(this.props.match.params);
// }
//
// componentWillReceiveProps (nextProps) {
// if (nextProps.match.params !== this.props.match.params) {
// this.props.onHandleShowHomepage(nextProps.match.params);
// }
// }
componentWillReceiveProps (nextProps) {
if (nextProps.match.params !== this.props.match.params) {
this.props.onHandleShowHomepage(nextProps.match.params);
}
componentWillUnmount () {
this.props.clearFile();
}
render () {
const { homeChannel } = this.props;
return homeChannel ? (

View file

@ -1,7 +1,8 @@
import { connect } from 'react-redux';
import View from './view';
const mapStateToProps = ({ show }) => {
const mapStateToProps = (props) => {
const {show} = props;
// select request info
const requestId = show.request.id;
// select asset info
@ -11,7 +12,7 @@ const mapStateToProps = ({ show }) => {
if (request && assetList) {
const assetKey = request.key; // note: just store this in the request
asset = assetList[assetKey] || null;
};
}
// return props
return {
asset,

View file

@ -1,6 +1,5 @@
import React from 'react';
import PageLayout from '@components/PageLayout';
import HorizontalSplit from '@components/HorizontalSplit';
import AssetTitle from '@containers/AssetTitle';
import AssetDisplay from '@containers/AssetDisplay';

View file

@ -41,6 +41,7 @@ const initialState = {
license : '',
nsfw : false,
},
isUpdate : false,
thumbnail: null,
thumbnailChannel,
thumbnailChannelId,
@ -49,7 +50,7 @@ const initialState = {
export default function (state = initialState, action) {
switch (action.type) {
case actions.FILE_SELECTED:
return Object.assign({}, initialState, { // note: clears to initial state
return Object.assign({}, state.isUpdate ? state : initialState, { // note: clears to initial state
file: action.data,
});
case actions.FILE_CLEAR:
@ -90,6 +91,10 @@ export default function (state = initialState, action) {
return Object.assign({}, state, {
thumbnail: action.data,
});
case actions.SET_UPDATE_TRUE:
return Object.assign({}, state, {
isUpdate: true,
});
default:
return state;
}

View file

@ -65,6 +65,33 @@ export default function (state = initialState, action) {
},
}),
});
case actions.ASSET_REMOVE:
const claim = action.data;
const newAssetList = state.assetList;
delete newAssetList[`a#${claim.name}#${claim.claimId}`];
const channelId = `c#${claim.channelName}#${claim.certificateId}`;
const channelClaims = state.channelList[channelId].claimsData.claims;
const newClaimsData = channelClaims.filter(c => c.claimId !== claim.claimId);
return Object.assign({}, state, {
assetList : newAssetList,
channelList: Object.assign({}, state.channelList, {
[channelId]: Object.assign({}, state.channelList[channelId], {
claimsData: Object.assign({}, state.channelList[channelId].claimsData, {
claims: newClaimsData,
}),
}),
}),
});
case actions.ASSET_UPDATE_CLAIMDATA:
return Object.assign({}, state, {
assetList: Object.assign({}, state.assetList, {
[action.data.id]: Object.assign({}, state.assetList[action.data.id], {
claimData: Object.assign({}, state.assetList[action.data.id].claimData, action.data.claimData),
}),
}),
});
// channel data
case actions.CHANNEL_ADD:
return Object.assign({}, state, {

View file

@ -0,0 +1,30 @@
import { call, put, takeLatest } from 'redux-saga/effects';
import * as actions from '../constants/publish_action_types';
import * as publishStates from '../constants/publish_claim_states';
import { updatePublishStatus, clearFile } from '../actions/publish';
import { removeAsset } from '../actions/show';
import { doAbandonClaim } from '../api/assetApi';
function * abandonClaim (action) {
const { claimData, history } = action.data;
const {claimId} = claimData;
const confirm = window.confirm('Are you sure you want to abandon this claim? This action cannot be undone.');
if (!confirm) return;
yield put(updatePublishStatus(publishStates.ABANDONING, 'Your claim is being abandoned...'));
try {
yield call(doAbandonClaim, claimId);
} catch (error) {
return console.log('abandon error:', error.message);
}
yield put(clearFile());
yield put(removeAsset(claimData));
return history.push('/');
}
export function * watchAbandonClaim () {
yield takeLatest(actions.ABANDON_CLAIM, abandonClaim);
};

View file

@ -5,37 +5,61 @@ import { updateError, updatePublishStatus, clearFile } from '../actions/publish'
import { selectPublishState } from '../selectors/publish';
import { selectChannelState } from '../selectors/channel';
import { selectSiteState } from '../selectors/site';
import { selectShowState, selectAsset } from '../selectors/show';
import { validateChannelSelection, validateNoPublishErrors } from '../utils/validate';
import { createPublishMetadata, createPublishFormData, createThumbnailUrl } from '../utils/publish';
import { makePublishRequestChannel } from '../channels/publish';
function * publishFile (action) {
const { history } = action.data;
const { publishInChannel, selectedChannel, file, claim, metadata, thumbnailChannel, thumbnailChannelId, thumbnail, error: publishToolErrors } = yield select(selectPublishState);
const publishState = yield select(selectPublishState);
const { publishInChannel, selectedChannel, file, claim, metadata, thumbnailChannel, thumbnailChannelId, thumbnail, isUpdate, error: publishToolErrors } = publishState;
const { loggedInChannel } = yield select(selectChannelState);
const { host } = yield select(selectSiteState);
let show, asset;
if (isUpdate) {
show = yield select(selectShowState);
asset = selectAsset(show);
}
// validate the channel selection
try {
validateChannelSelection(publishInChannel, selectedChannel, loggedInChannel);
} catch (error) {
return yield put(updateError('channel', error.message));
};
}
// validate publish parameters
try {
validateNoPublishErrors(publishToolErrors);
} catch (error) {
return console.log('publish error:', error.message);
}
// create metadata
let publishMetadata = createPublishMetadata(claim, file, metadata, publishInChannel, selectedChannel);
if (thumbnail) {
// add thumbnail to publish metadata
publishMetadata['thumbnail'] = createThumbnailUrl(thumbnailChannel, thumbnailChannelId, claim, host);
let publishMetadata, publishFormData, publishChannel;
if (isUpdate) {
publishMetadata = createPublishMetadata(asset.name, {type: asset.claimData.contentType}, metadata, publishInChannel, selectedChannel);
publishMetadata['channelName'] = asset.claimData.channelName;
if (thumbnail) {
// add thumbnail to publish metadata
publishMetadata['thumbnail'] = createThumbnailUrl(thumbnailChannel, thumbnailChannelId, claim, host);
}
// create form data for main publish
publishFormData = createPublishFormData(file, thumbnail, publishMetadata);
// make the publish request
publishChannel = yield call(makePublishRequestChannel, publishFormData, true);
} else {
// create metadata
publishMetadata = createPublishMetadata(claim, file, metadata, publishInChannel, selectedChannel);
if (thumbnail) {
// add thumbnail to publish metadata
publishMetadata['thumbnail'] = createThumbnailUrl(thumbnailChannel, thumbnailChannelId, claim, host);
}
// create form data for main publish
publishFormData = createPublishFormData(file, thumbnail, publishMetadata);
// make the publish request
publishChannel = yield call(makePublishRequestChannel, publishFormData);
}
// create form data for main publish
const publishFormData = createPublishFormData(file, thumbnail, publishMetadata);
// make the publish request
const publishChannel = yield call(makePublishRequestChannel, publishFormData);
while (true) {
const {loadStart, progress, load, success, error: publishError} = yield take(publishChannel);
if (publishError) {
@ -43,6 +67,15 @@ function * publishFile (action) {
}
if (success) {
yield put(clearFile());
if (isUpdate) {
yield put({
type: 'ASSET_UPDATE_CLAIMDATA',
data: {
id : `a#${success.data.name}#${success.data.claimId}`,
claimData: success.data.claimData,
},
});
}
return history.push(`/${success.data.claimId}/${success.data.name}`);
}
if (loadStart) {
@ -55,7 +88,7 @@ function * publishFile (action) {
yield put(updatePublishStatus(publishStates.PUBLISHING, null));
}
}
};
}
export function * watchPublishStart () {
yield takeLatest(actions.PUBLISH_START, publishFile);

View file

@ -10,6 +10,7 @@ import { watchUpdateChannelAvailability } from './updateChannelAvailability';
import { watchChannelCreate } from './createChannel';
import { watchChannelLoginCheck } from './checkForLoggedInChannel';
import { watchChannelLogout } from './logoutChannel';
import { watchAbandonClaim } from './abandon';
export function * rootSaga () {
yield all([
@ -27,5 +28,6 @@ export function * rootSaga () {
watchChannelLoginCheck(),
watchChannelLogout(),
watchUpdateAssetViews(),
watchAbandonClaim(),
]);
}

View file

@ -16,7 +16,9 @@ export const createPublishMetadata = (claim, { type }, { title, description, lic
export const createPublishFormData = (file, thumbnail, metadata) => {
let fd = new FormData();
// append file
fd.append('file', file);
if (file) {
fd.append('file', file);
}
// append thumbnail
if (thumbnail) {
fd.append('thumbnail', thumbnail);
@ -31,5 +33,5 @@ export const createPublishFormData = (file, thumbnail, metadata) => {
};
export const createThumbnailUrl = (channel, channelId, claim, host) => {
return `${host}/${channel}:${channelId}/${claim}-thumb.png`;
return `${host}/${channel}:${channelId}/${claim}-thumb.jpg`;
};

View file

@ -0,0 +1,44 @@
const logger = require('winston');
const db = require('../../../../models');
const { abandonClaim } = require('../../../../lbrynet');
const deleteFile = require('../publish/deleteFile.js');
const authenticateUser = require('../publish/authentication.js');
/*
route to abandon a claim through the daemon
*/
const claimAbandon = async (req, res) => {
const {claimId} = req.body;
const {user} = req;
try {
const [channel, claim] = await Promise.all([
authenticateUser(user.channelName, null, null, user),
db.Claim.findOne({where: {claimId}}),
]);
if (!claim) throw new Error('That channel does not exist');
if (!channel.channelName) throw new Error('You don\'t own this channel');
await abandonClaim({claimId});
const file = await db.File.findOne({where: {claimId}});
await Promise.all([
deleteFile(file.filePath),
db.File.destroy({where: {claimId}}),
db.Claim.destroy({where: {claimId}}),
]);
logger.debug(`Claim abandoned: ${claimId}`);
res.status(200).json({
success: true,
message: `Claim with id ${claimId} abandonded`,
});
} catch (error) {
logger.error('abandon claim error:', error);
res.status(400).json({
success: false,
message: error.message,
});
}
};
module.exports = claimAbandon;

View file

@ -14,7 +14,7 @@ const publish = require('./publish.js');
const createPublishParams = require('./createPublishParams.js');
const createThumbnailPublishParams = require('./createThumbnailPublishParams.js');
const parsePublishApiRequestBody = require('./parsePublishApiRequestBody.js');
const parsePublishApiRequestFiles = require('./parsePublishApiRequestFiles.js');
const {parsePublishApiRequestFiles} = require('./parsePublishApiRequestFiles.js');
const authenticateUser = require('./authentication.js');
const CLAIM_TAKEN = 'CLAIM_TAKEN';
@ -85,17 +85,18 @@ const claimPublish = ({ body, files, headers, ip, originalUrl, user, tor }, res)
// publish the asset
return publish(publishParams, fileName, fileType);
})
.then(result => {
.then(claimData => {
logger.debug('Publish success >', claimData);
res.status(200).json({
success: true,
message: 'publish completed successfully',
data : {
name,
claimId : result.claim_id,
url : `${host}/${result.claim_id}/${name}`, // for backwards compatability with app
showUrl : `${host}/${result.claim_id}/${name}`,
serveUrl: `${host}/${result.claim_id}/${name}${fileExtension}`,
lbryTx : result,
claimId : claimData.claimId,
url : `${host}/${claimData.claimId}/${name}`, // for backwards compatability with app
showUrl : `${host}/${claimData.claimId}/${name}`,
serveUrl: `${host}/${claimData.claimId}/${name}${fileExtension}`,
claimData,
},
});
// record the publish end time and send to google analytics

View file

@ -42,4 +42,61 @@ const parsePublishApiRequestFiles = ({file, thumbnail}) => {
};
};
module.exports = parsePublishApiRequestFiles;
const parsePublishApiRequestFile = ({file, thumbnail}, isUpdate) => {
// make sure a file was provided
if (!file) {
if (isUpdate) {
return;
}
throw new Error('no file with key of [file] found in request');
}
if (!file.path) {
throw new Error('no file path found');
}
if (!file.type) {
throw new Error('no file type found');
}
if (!file.size) {
throw new Error('no file size found');
}
// validate the file name
if (!file.name) {
throw new Error('no file name found');
}
if (file.name.indexOf('.') < 0) {
throw new Error('no file extension found in file name');
}
if (file.name.indexOf('.') === 0) {
throw new Error('file name cannot start with "."');
}
if (/'/.test(file.name)) {
throw new Error('apostrophes are not allowed in the file name');
}
// validate the file
validateFileTypeAndSize(file);
// return results
return {
fileName : file.name,
filePath : file.path,
fileExtension: path.extname(file.path),
fileType : file.type,
};
};
const parsePublishApiRequestThumbnail = ({file, thumbnail}) => {
if (!thumbnail) {
return;
}
return {
thumbnailFileName: thumbnail.name,
thumbnailFilePath: thumbnail.path,
thumbnailFileType: thumbnail.type,
};
};
module.exports = {
parsePublishApiRequestFiles,
parsePublishApiRequestFile,
parsePublishApiRequestThumbnail,
};

View file

@ -1,81 +1,62 @@
const logger = require('winston');
const { publishClaim } = require('../../../../lbrynet');
const db = require('../../../../models');
const { publishClaim } = require('../../../../lbrynet');
const { createFileRecordDataAfterPublish } = require('../../../../models/utils/createFileRecordData.js');
const { createClaimRecordDataAfterPublish } = require('../../../../models/utils/createClaimRecordData.js');
const deleteFile = require('./deleteFile.js');
const publish = (publishParams, fileName, fileType) => {
return new Promise((resolve, reject) => {
let publishResults, certificateId, channelName;
// publish the file
return publishClaim(publishParams)
.then(result => {
logger.info(`Successfully published ${publishParams.name} ${fileName}`, result);
const publish = async (publishParams, fileName, fileType) => {
let publishResults;
let channel;
let fileRecord;
let filePath = publishParams.file_path;
let newFile = Boolean(filePath);
// Support new daemon, TODO: remove
publishResults = result.output && result.output.claim_id ? result.output : result;
try {
publishResults = await publishClaim(publishParams);
logger.info(`Successfully published ${publishParams.name} ${fileName}`, publishResults);
// get the channel information
if (publishParams.channel_name) {
logger.debug(`this claim was published in channel: ${publishParams.channel_name}`);
return db.Channel.findOne({
where: {
channelName: publishParams.channel_name,
},
});
} else {
logger.debug('this claim was not published in a channel');
return null;
}
})
.then(channel => {
// set channel information
certificateId = null;
channelName = null;
if (channel) {
certificateId = channel.channelClaimId;
channelName = channel.channelName;
}
logger.debug(`certificateId: ${certificateId}`);
})
.then(() => {
return Promise.all([
createFileRecordDataAfterPublish(fileName, fileType, publishParams, publishResults),
createClaimRecordDataAfterPublish(certificateId, channelName, fileName, fileType, publishParams, publishResults),
]);
})
.then(([fileRecord, claimRecord]) => {
// upsert the records
const {name} = publishParams;
const {claim_id: claimId} = publishResults;
const upsertCriteria = {
name,
claimId,
};
return Promise.all([
db.upsert(db.File, fileRecord, upsertCriteria, 'File'),
db.upsert(db.Claim, claimRecord, upsertCriteria, 'Claim'),
]);
})
.then(([file, claim]) => {
logger.debug('File and Claim records successfully created');
return Promise.all([
file.setClaim(claim),
claim.setFile(file),
]);
})
.then(() => {
logger.debug('File and Claim records successfully associated');
// resolve the promise with the result from lbryApi publishClaim;
resolve(publishResults);
})
.catch(error => {
logger.error('PUBLISH ERROR', error);
deleteFile(publishParams.file_path); // delete the local file
reject(error);
// get the channel information
if (publishParams.channel_name) {
logger.debug(`this claim was published in channel: ${publishParams.channel_name}`);
channel = await db.Channel.findOne({
where: {
channelName: publishParams.channel_name,
},
});
});
} else {
channel = null;
}
const certificateId = channel ? channel.channelClaimId : null;
const channelName = channel ? channel.channelName : null;
const claimRecord = await createClaimRecordDataAfterPublish(certificateId, channelName, fileName, fileType, publishParams, publishResults);
const {claimId} = claimRecord;
const upsertCriteria = {name: publishParams.name, claimId};
if (newFile) {
fileRecord = await createFileRecordDataAfterPublish(fileName, fileType, publishParams, publishResults);
} else {
fileRecord = await db.File.findOne({where: {claimId}});
}
const [file, claim] = await Promise.all([
db.upsert(db.File, fileRecord, upsertCriteria, 'File'),
db.upsert(db.Claim, claimRecord, upsertCriteria, 'Claim'),
]);
logger.info('File and Claim records successfully created');
await Promise.all([
file.setClaim(claim),
claim.setFile(file),
]);
logger.info('File and Claim records successfully associated');
return claimRecord;
} catch (err) {
logger.error('PUBLISH ERROR', err);
await deleteFile(filePath);
return err;
}
};
module.exports = publish;

View file

@ -0,0 +1,151 @@
const logger = require('winston');
const db = require('../../../../models');
const { details, publishing: { disabled, disabledMessage, primaryClaimAddress } } = require('@config/siteConfig');
const { resolveUri } = require('../../../../lbrynet');
const { sendGATimingEvent } = require('../../../../utils/googleAnalytics.js');
const { handleErrorResponse } = require('../../../utils/errorHandlers.js');
const publish = require('../publish/publish.js');
const parsePublishApiRequestBody = require('../publish/parsePublishApiRequestBody');
const {parsePublishApiRequestFile, parsePublishApiRequestThumbnail} = require('../publish/parsePublishApiRequestFiles.js');
const authenticateUser = require('../publish/authentication.js');
const createThumbnailPublishParams = require('../publish/createThumbnailPublishParams.js');
/*
route to update a claim through the daemon
*/
const updateMetadata = ({nsfw, license, title, description}) => {
const update = {};
if (nsfw) update['nsfw'] = nsfw;
if (license) update['license'] = license;
if (title) update['title'] = title;
if (description) update['description'] = description;
return update;
};
const claimUpdate = ({ body, files, headers, ip, originalUrl, user, tor }, res) => {
// logging
logger.info('Claim update request:', {
ip,
headers,
body,
files,
user,
});
// check for disabled publishing
if (disabled) {
return res.status(503).json({
success: false,
message: disabledMessage,
});
}
// define variables
let channelName;
let channelId;
let channelPassword;
let description;
let fileName;
let filePath;
let fileType;
let gaStartTime;
let thumbnail;
let fileExtension;
let license;
let name;
let nsfw;
let thumbnailFileName;
let thumbnailFilePath;
let thumbnailFileType;
let title;
let claimRecord;
let metadata;
// record the start time of the request
gaStartTime = Date.now();
try {
({name, nsfw, license, title, description, thumbnail} = parsePublishApiRequestBody(body));
if (files.file) {
({fileName, filePath, fileExtension, fileType} = parsePublishApiRequestFile(files));
if (files.thumbnail) {
({thumbnailFileName, thumbnailFilePath, thumbnailFileType} = parsePublishApiRequestThumbnail(files));
}
}
({channelName, channelId, channelPassword} = body);
} catch (error) {
return res.status(400).json({success: false, message: error.message});
}
// check channel authorization
authenticateUser(channelName, channelId, channelPassword, user)
.then(({ channelName, channelClaimId }) => {
return db.Claim.findOne({
where: {
name,
channelName,
},
});
})
.then(claim => {
return resolveUri(`${claim.name}#${claim.claimId}`);
})
.then(fullClaim => {
claimRecord = fullClaim;
logger.info('fullClaim', fullClaim);
metadata = Object.assign({}, {
title : claimRecord.title,
description: claimRecord.description,
nsfw : claimRecord.nsfw,
license : claimRecord.license,
language : 'en',
author : details.title,
}, updateMetadata({title, description, nsfw, license}));
const publishParams = {
name,
bid : 0.01,
claim_address: primaryClaimAddress,
channel_name : channelName,
channel_id : channelId,
metadata,
};
if (files.file) {
publishParams['file_path'] = filePath;
} else {
fileName = fullClaim.file_name;
fileType = fullClaim.mime_type;
publishParams['sources'] = fullClaim.claim.value.stream.source;
}
// publish the thumbnail, if one exists
if (thumbnailFileName) {
const thumbnailPublishParams = createThumbnailPublishParams(thumbnailFilePath, name, license, nsfw);
publish(thumbnailPublishParams, thumbnailFileName, thumbnailFileType);
publishParams['thumbnail'] = `${details.host}/${channelName}:${channelId}/${name}-thumb.jpg`;
}
return publish(publishParams, fileName, fileType);
})
.then(claimData => {
const {claimId} = claimData;
res.status(200).json({
success: true,
message: 'update successful',
data : {
name,
channelName,
channelId: claimData.certificateId,
claimId,
url : `${details.host}/${claimId}/${name}`, // for backwards compatability with app
showUrl : `${details.host}/${claimId}/${name}`,
claimData,
},
});
// record the publish end time and send to google analytics
sendGATimingEvent('end-to-end', 'update', fileType, gaStartTime, Date.now());
})
.catch(error => {
handleErrorResponse(originalUrl, ip, error, res);
});
};
module.exports = claimUpdate;

View file

@ -0,0 +1,56 @@
const path = require('path');
const validateFileTypeAndSize = require('../publish/validateFileTypeAndSize.js');
const parseUpdateFile = ({file}) => {
// make sure a file was provided
if (!file) {
return false;
}
if (!file.path) {
throw new Error('no file path found');
}
if (!file.type) {
throw new Error('no file type found');
}
if (!file.size) {
throw new Error('no file size found');
}
// validate the file name
if (!file.name) {
throw new Error('no file name found');
}
if (file.name.indexOf('.') < 0) {
throw new Error('no file extension found in file name');
}
if (file.name.indexOf('.') === 0) {
throw new Error('file name cannot start with "."');
}
if (/'/.test(file.name)) {
throw new Error('apostrophes are not allowed in the file name');
}
// validate the file
validateFileTypeAndSize(file);
// return results
return {
fileName : file.name,
filePath : file.path,
fileExtension: path.extname(file.path),
fileType : file.type,
};
};
const parseUpdateThumbnail = ({thumbnail}) => {
if (!thumbnail) {
return false;
}
return {
thumbnailFileName: thumbnail.name,
thumbnailFilePath: thumbnail.path,
thumbnailFileType: thumbnail.type,
};
};
module.exports = [
parseUpdateFile,
parseUpdateThumbnail,
];

View file

@ -46,6 +46,21 @@ module.exports = {
});
});
},
async abandonClaim ({claimId}) {
logger.debug(`lbryApi >> Abandon claim "${claimId}"`);
const gaStartTime = Date.now();
try {
const abandon = await axios.post(lbrynetUri, {
method: 'claim_abandon',
params: { claim_id: claimId },
});
sendGATimingEvent('lbrynet', 'abandonClaim', 'ABANDON_CLAIM', gaStartTime, Date.now());
return abandon.data;
} catch (error) {
logger.error(error);
return error;
}
},
getClaimList (claimName) {
logger.debug(`lbryApi >> Getting claim_list for "${claimName}"`);
const gaStartTime = Date.now();

View file

@ -13,6 +13,8 @@ const claimGet = require('../../controllers/api/claim/get');
const claimList = require('../../controllers/api/claim/list');
const claimLongId = require('../../controllers/api/claim/longId');
const claimPublish = require('../../controllers/api/claim/publish');
const claimAbandon = require('../../controllers/api/claim/abandon');
const claimUpdate = require('../../controllers/api/claim/update');
const claimResolve = require('../../controllers/api/claim/resolve');
const claimShortId = require('../../controllers/api/claim/shortId');
const claimViews = require('../../controllers/api/claim/views');
@ -29,12 +31,10 @@ const getOEmbedData = require('../../controllers/api/oEmbed');
module.exports = {
// homepage routes
'/api/homepage/data/channels': { controller: [ torCheckMiddleware, channelData ] },
// channel routes
'/api/channel/availability/:name': { controller: [ torCheckMiddleware, channelAvailability ] },
'/api/channel/short-id/:longId/:name': { controller: [ torCheckMiddleware, channelShortId ] },
'/api/channel/data/:channelName/:channelClaimId': { controller: [ torCheckMiddleware, channelData ] },
'/api/channel/data/:channelName/:channelClaimId': { controller: [ torCheckMiddleware, channelData ] },
'/api/channel/claims/:channelName/:channelClaimId/:page': { controller: [ torCheckMiddleware, channelClaims ] },
// sepcial routes
@ -47,6 +47,8 @@ module.exports = {
'/api/claim/list/:name': { controller: [ torCheckMiddleware, claimList ] },
'/api/claim/long-id': { method: 'post', controller: [ torCheckMiddleware, claimLongId ] }, // note: should be a 'get'
'/api/claim/publish': { method: 'post', controller: [ torCheckMiddleware, autoblockPublishMiddleware, multipartMiddleware, autoblockPublishBodyMiddleware, claimPublish ] },
'/api/claim/update': { method: 'post', controller: [ torCheckMiddleware, multipartMiddleware, claimUpdate ] },
'/api/claim/abandon': { method: 'post', controller: [ torCheckMiddleware, multipartMiddleware, claimAbandon ] },
'/api/claim/resolve/:name/:claimId': { controller: [ torCheckMiddleware, claimResolve ] },
'/api/claim/short-id/:longId/:name': { controller: [ torCheckMiddleware, claimShortId ] },
'/api/claim/views/:claimId': { controller: [ torCheckMiddleware, claimViews ] },

View file

@ -15,6 +15,7 @@ module.exports = {
'/trending': { controller: redirect('/popular') },
'/popular': { controller: handlePageRequest },
'/new': { controller: handlePageRequest },
'/edit/:claimId': { controller: handlePageRequest },
'/multisite': { controller: handlePageRequest },
'/video-embed/:name/:claimId/:config?': { controller: handleVideoEmbedRequest }, // for twitter
};