Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hotfix/disk content type #155

Merged
merged 12 commits into from Nov 6, 2018
2 changes: 1 addition & 1 deletion hub/package.json
@@ -1,6 +1,6 @@
{
"name": "gaia-hub",
"version": "2.2.1",
"version": "2.2.2",
"description": "",
"main": "index.js",
"engines": {
Expand Down
21 changes: 20 additions & 1 deletion hub/src/server/drivers/diskDriver.js
@@ -1,6 +1,6 @@
/* @flow */
import fs from 'fs'
import { BadPathError } from '../errors'
import { BadPathError, InvalidInputError } from '../errors'
import logger from 'winston'
import Path from 'path'

Expand All @@ -11,6 +11,8 @@ type DISK_CONFIG_TYPE = { diskSettings: { storageRootDirectory?: string },
pageSize?: number,
readURL?: string }

const METADATA_DIRNAME = '.gaia-metadata'

class DiskDriver implements DriverModel {
storageRootDirectory: string
readURL: string
Expand Down Expand Up @@ -135,11 +137,17 @@ class DiskDriver implements DriverModel {
.then(() => {
const path = args.path
const topLevelDir = args.storageTopLevel
const contentType = args.contentType

if (!topLevelDir) {
throw new BadPathError('Invalid Path')
}

if (contentType.length > 255) {
// no way this is valid
throw new InvalidInputError('Invalid content-type')
}

const abspath = Path.join(this.storageRootDirectory, topLevelDir, path)
if (!DiskDriver.isPathValid(abspath)) {
throw new BadPathError('Invalid Path')
Expand All @@ -163,6 +171,17 @@ class DiskDriver implements DriverModel {
const writePipe = fs.createWriteStream(abspath, { mode: 0o600, flags: 'w' })
args.stream.pipe(writePipe)

// remember content type in $storageRootDir/.gaia-metadata/$address/$path
// (i.e. these files are outside the address bucket, and are thus hidden)
const contentTypePath = Path.join(
this.storageRootDirectory, METADATA_DIRNAME, topLevelDir, path)

const contentTypeDirPath = Path.dirname(contentTypePath)
this.mkdirs(contentTypeDirPath)

fs.writeFileSync(
contentTypePath, JSON.stringify({ 'content-type': contentType }), { mode: 0o600 })

return `${this.readURL}${Path.join(topLevelDir, path)}`
})
}
Expand Down
8 changes: 7 additions & 1 deletion hub/src/server/errors.js
Expand Up @@ -21,4 +21,10 @@ export class NotEnoughProofError {
}
}


export class InvalidInputError {
constructor (message) {
this.name = 'InvalidInputError'
this.message = message
this.stack = (new Error(message)).stack
}
}
8 changes: 7 additions & 1 deletion hub/test/test.js
Expand Up @@ -6,6 +6,7 @@ const FetchMock = require('fetch-mock')
let request = require('supertest')
let bitcoin = require('bitcoinjs-lib')
let fs = require('fs')
let Path = require('path')

const { Readable, Writable } = require('stream');

Expand Down Expand Up @@ -534,9 +535,10 @@ function testDiskDriver() {
const DiskDriver = require(diskDriverImport)

test('diskDriver', (t) => {
t.plan(4)
t.plan(5)
const driver = new DiskDriver(config)
const prefix = driver.getReadURLPrefix()
const storageDir = driver.storageRootDirectory
const s = new Readable()
s._read = function noop() {}
s.push('hello world')
Expand All @@ -553,7 +555,11 @@ function testDiskDriver() {
contentType: 'application/octet-stream',
contentLength: 12 }))
.then((readUrl) => {
const filePath = Path.join(storageDir, '12345', 'foo/bar.txt')
const metadataPath = Path.join(storageDir, '.gaia-metadata', '12345', 'foo/bar.txt')
t.ok(readUrl.startsWith(prefix + '12345'), `${readUrl} must start with readUrlPrefix ${prefix}12345`)
t.equal(JSON.parse(fs.readFileSync(metadataPath).toString())['content-type'], 'application/octet-stream',
'Content-type metadata was written')
})
.then(() => driver.listFiles('12345'))
.then((files) => {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -4,6 +4,6 @@
"node": "8.1.x"
},
"scripts": {
"postinstall": "NODE_ENV=development npm install --prefix hub"
"postinstall": "NODE_ENV=development npm install --prefix hub && NODE_ENV=development npm install --prefix reader"
}
}
3 changes: 3 additions & 0 deletions reader/.babelrc
@@ -0,0 +1,3 @@
{
"presets": ["env", "flow"]
}
33 changes: 33 additions & 0 deletions reader/.eslintrc
@@ -0,0 +1,33 @@
{
"parser": "babel-eslint",
"plugins": ["flowtype"],
"env": {
"browser": false,
"es6": true,
"node": true,
},
"rules": {
"comma-dangle": ["error", "never"],
"quotes": [2, "single"],
"eol-last": 2,
"no-debugger": 1,
"no-mixed-requires": 0,
"no-underscore-dangle": 0,
"no-multi-spaces": 0,
"no-trailing-spaces": 0,
"no-extra-boolean-cast": 0,
"no-undef": 2,
"no-unused-vars": 2,
"no-var": 2,
"no-param-reassign": 0,
"no-else-return": 0,
"no-console": 0,
"prefer-const": 2,
"new-cap": 0,
"camelcase": 2,
"brace-style": 2,
"semi": [2, "never"],
"flowtype/define-flow-type": 1,
"valid-jsdoc": ["error"]
}
}
12 changes: 12 additions & 0 deletions reader/.flowconfig
@@ -0,0 +1,12 @@
[ignore]
.*/node_modules/documentation/*
.*/node_modules/.staging/*
.*/node_modules/npm/*

[include]

[libs]

[options]

[lints]
51 changes: 51 additions & 0 deletions reader/package.json
@@ -0,0 +1,51 @@
{
"name": "gaia-reader",
"version": "2.2.2",
"description": "",
"main": "index.js",
"engines": {
"node": "^8"
},
"dependencies": {
"body-parser": "^1.18.1",
"cors": "^2.8.4",
"express": "^4.15.4",
"express-winston": "^2.4.0",
"node-fetch": "^2.0.0",
"winston": "^2.3.1"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-eslint": "^8.2.2",
"babel-preset-env": "^1.6.1",
"babel-preset-flow": "^6.23.0",
"eslint": "^4.18.1",
"eslint-plugin-flowtype": "^2.46.3",
"fetch-mock": "^6.4.2",
"flow-bin": "^0.71.0",
"nock": "^9.1.9",
"nyc": "^13.0.0",
"proxyquire": "^2.0.1",
"supertest": "^3.0.0",
"tape": "^4.9.0"
},
"bin": {
"blockstack-gaia-reader": "./lib/index.js"
},
"scripts": {
"start": "npm run build && node lib/index.js",
"build": "npm run lint && babel src -d lib && chmod +x lib/index.js",
"flow": "flow",
"lint": "eslint src",
"test-inner": "npm run build && npm run flow && node ./test/test.js",
"test": "nyc --reporter=text npm run test-inner"
},
"repository": {
"type": "git",
"url": "https://github.com/blockstack/gaia.git"
},
"authors": [
"Aaron Blankstein (aaron@blockstack.com)",
"Jude Nelson (jude@blockstack.com)"
]
}
37 changes: 37 additions & 0 deletions reader/src/config.js
@@ -0,0 +1,37 @@
import winston from 'winston'
import fs from 'fs'
import process from 'process'

const configDefaults = {
argsTransport: {
level: 'debug',
handleExceptions: true,
timestamp: true,
stringify: true,
colorize: true,
json: true
},
regtest: false,
testnet: false,
port: 8008,
diskSettings: {
storageRootDirectory: '/tmp/gaia-disk'
}
}

export function getConfig() {
const configPath = process.env.CONFIG_PATH || process.argv[2] || './config.json'
let config
try {
config = Object.assign(
{}, configDefaults, JSON.parse(fs.readFileSync(configPath).toString()))
} catch (e) {
config = Object.assign({}, configDefaults)
}

winston.configure({ transports: [
new winston.transports.Console(config.argsTransport) ] })

return config
}

58 changes: 58 additions & 0 deletions reader/src/http.js
@@ -0,0 +1,58 @@
/* @flow */

import express from 'express'
import expressWinston from 'express-winston'
import logger from 'winston'
import cors from 'cors'
import Path from 'path'

import {
GaiaDiskReader
} from './server'

export function makeHttpServer(config: Object) {
const app = express()
const server = new GaiaDiskReader(config)

app.config = config

app.use(expressWinston.logger({
transports: logger.loggers.default.transports }))

app.use(cors())

app.get(/\/([a-zA-Z0-9]+)\/(.+)/, (req: express.request, res: express.response) => {
let filename = req.params[1]
if (filename.endsWith('/')) {
filename = filename.substring(0, filename.length - 1)
}
const address = req.params[0]

return server.handleGet(address, filename)
.then((fileInfo) => {
const exists = fileInfo.exists
const contentType = fileInfo.contentType

if (!exists) {
return res.status(404).send('File not found')
}

const opts = {
root: config.storageRootDirectory,
headers: {
'content-type': contentType
}
}
const path = Path.join(address, filename)

return res.sendFile(path, opts)
})
.catch((err) => {
logger.error(err)
return res.status(400).send('Could not return file')
})
})

return app
}

12 changes: 12 additions & 0 deletions reader/src/index.js
@@ -0,0 +1,12 @@
#!/usr/bin/env node
import winston from 'winston'
import { makeHttpServer } from './http.js'
import { getConfig } from './config.js'

const conf = getConfig()
const app = makeHttpServer(conf)

app.listen(
app.config.port,
() => winston.warn(`server starting on port ${app.config.port} in ${app.settings.env} mode`))

42 changes: 42 additions & 0 deletions reader/src/server.js
@@ -0,0 +1,42 @@
/* @flow */

import Path from 'path'
import fs from 'fs'

const METADATA_DIRNAME = '.gaia-metadata'

export class GaiaDiskReader {

config: Object

constructor(config: Object) {
this.config = config
}

handleGet(topLevelDir: string, filename: string)
: Promise<{ exists: boolean, contentType: ?string } > {
const storageRoot = this.config.diskSettings.storageRootDirectory
if (!storageRoot) {
throw new Error('Misconfiguration: no storage root set')
}

const filePath = Path.join(storageRoot, topLevelDir, filename)
try {
fs.statSync(filePath)
} catch (e) {
const ret = { exists: false, contentType: undefined }
return Promise.resolve().then(() => ret)
}

const metadataPath = Path.join(storageRoot, METADATA_DIRNAME, topLevelDir, filename)
try {
const metadataJSON = fs.readFileSync(metadataPath).toString()
const metadata = JSON.parse(metadataJSON)
const ret = { exists: true, contentType: metadata['content-type'] }
return Promise.resolve().then(() => ret)
} catch (e) {
const ret = { exists: true, contentType: 'application/octet-stream' }
return Promise.resolve().then(() => ret)
}
}
}