├── .gitignore ├── assets ├── favicon.ico ├── app.js └── style.styl ├── dist ├── assets │ ├── favicon.ico │ ├── app.js │ └── style.css └── bin │ └── gallery.js ├── LICENSE ├── gulpfile.babel.js ├── views └── index.hbs ├── README.md ├── package.json └── bin └── gallery.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreasmcdermott/micro-gallery/HEAD/assets/favicon.ico -------------------------------------------------------------------------------- /dist/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreasmcdermott/micro-gallery/HEAD/dist/assets/favicon.ico -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Andreas McDermott 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp' 2 | import babel from 'gulp-babel' 3 | import cached from 'gulp-cached' 4 | import chmod from 'gulp-chmod' 5 | import stylus from 'gulp-stylus' 6 | 7 | gulp.task('js-bin', () => 8 | gulp 9 | .src('./bin/*.js') 10 | .pipe(cached('bin')) 11 | .pipe(babel()) 12 | .pipe(chmod(755)) 13 | .pipe(gulp.dest('./dist/bin')) 14 | ) 15 | 16 | gulp.task('js-client', () => 17 | gulp 18 | .src('./assets/app.js') 19 | .pipe(babel()) 20 | .pipe(cached('clientjs')) 21 | .pipe(gulp.dest('./dist/assets')) 22 | ) 23 | 24 | gulp.task('stylus', () => 25 | gulp.src('./assets/*.styl').pipe(stylus()).pipe(gulp.dest('./dist/assets')) 26 | ) 27 | 28 | gulp.task('other-assets', () => 29 | gulp.src('./assets/favicon.ico').pipe(gulp.dest('./dist/assets')) 30 | ) 31 | 32 | gulp.task('watch-js', () => { 33 | gulp.watch('./bin/*.js', ['js-bin']) 34 | gulp.watch('./assets/*.js', ['js-client']) 35 | }) 36 | 37 | gulp.task('watch-stylus', () => { 38 | gulp.watch('./assets/*.styl', ['stylus']) 39 | }) 40 | 41 | gulp.task('watch', ['watch-js', 'watch-stylus']) 42 | gulp.task('build', ['js-bin', 'js-client', 'stylus', 'other-assets']) 43 | 44 | gulp.task('default', ['build', 'watch']) 45 | -------------------------------------------------------------------------------- /views/index.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Images in {{folder}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

Images in  {{#each path}} 16 | {{name}} {{/each}} 17 |

18 | 19 | 24 | 25 | 30 | 31 |
32 | 33 | 34 | 35 |
36 |
37 | 38 | 39 | { 2 | document.body.className = 'zoomed' 3 | 4 | const zoomedImg = document.getElementById('zoomedImage') 5 | zoomedImg.parentNode.parentNode.focus() 6 | zoomedImg.setAttribute('src', img.getAttribute('src')) 7 | zoomedImg.setAttribute('title', img.getAttribute('title')) 8 | zoomedImg.setAttribute('alt', img.getAttribute('alt')) 9 | 10 | zoomedImg.setAttribute('style', 11 | `margin: ${(window.innerHeight - zoomedImg.height) / 2}px ${(window.innerWidth - zoomedImg.width) / 2}px;`) 12 | } 13 | 14 | const unzoom = () => { 15 | document.body.className = '' 16 | } 17 | 18 | window.onload = () => { 19 | document.body.addEventListener('click', e => { 20 | if (e.target.className === 'images__img') { 21 | zoom(e.target) 22 | } else if (e.target.className === 'images__zoom') { 23 | zoom(e.target.firstChild) 24 | } else if (e.target.className.indexOf('js-unzoom') >= 0) { 25 | unzoom() 26 | } else { 27 | return 28 | } 29 | 30 | e.preventDefault() 31 | 32 | }, {capture: true}) 33 | 34 | const TAB = 9 35 | const ESC = 27 36 | document.body.addEventListener('keydown', e => { 37 | if (document.body.className === 'zoomed') { 38 | if (e.which === TAB) { 39 | document.getElementById('zoomedImage').parentNode.focus() 40 | } else if (e.which === ESC) { 41 | unzoom() 42 | } else { 43 | return 44 | } 45 | 46 | e.preventDefault() 47 | } 48 | }) 49 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # micro-gallery 2 | Like [zeit's serve](https://github.com/zeit/serve), but for images. 3 | 4 | Share you folder(s) of images with a single command. 5 | 6 | ## Demo 7 | 8 | https://micro-gallery.now.sh 9 | 10 | ### Demo source 11 | 12 | https://micro-gallery.now.sh/_src 13 | 14 | There isn't much "source" to talk about though, it is mainly a folder of images and a package.json file. 15 | 16 | ## Usage 17 | Install it (requires Node v6.0.0 and above) 18 | 19 | ``` 20 | $ npm install -g micro-gallery 21 | ``` 22 | 23 | Run it 24 | 25 | ``` 26 | $ gallery [options] 27 | ``` 28 | 29 | ### Options 30 | 31 | | Usage | Description | Default value | 32 | | ---------------------- | ----------- | ------------------ | 33 | | -h, --help | Output all available options | - | 34 | | -v, --version | The version tag of the micro-gallery instance on your device | - | 35 | | -p, --port [port] | A custom port on which the app will be running | 3000 | 36 | | -d, --dev | Use development mode. When active assets and template aren't cached | - | 37 | 38 | ## Contribute 39 | 40 | 1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device 41 | 2. Uninstall now-serve if it's already installed: `npm uninstall -g micro-gallery` 42 | 3. Link it to the global module directory: `npm link` 43 | 4. Transpile the source code and watch for changes: `npm start` 44 | -------------------------------------------------------------------------------- /dist/assets/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const zoom = img => { 4 | document.body.className = 'zoomed'; 5 | 6 | const zoomedImg = document.getElementById('zoomedImage'); 7 | zoomedImg.parentNode.parentNode.focus(); 8 | zoomedImg.setAttribute('src', img.getAttribute('src')); 9 | zoomedImg.setAttribute('title', img.getAttribute('title')); 10 | zoomedImg.setAttribute('alt', img.getAttribute('alt')); 11 | 12 | zoomedImg.setAttribute('style', `margin: ${ (window.innerHeight - zoomedImg.height) / 2 }px ${ (window.innerWidth - zoomedImg.width) / 2 }px;`); 13 | }; 14 | 15 | const unzoom = () => { 16 | document.body.className = ''; 17 | }; 18 | 19 | window.onload = () => { 20 | document.body.addEventListener('click', e => { 21 | if (e.target.className === 'images__img') { 22 | zoom(e.target); 23 | } else if (e.target.className === 'images__zoom') { 24 | zoom(e.target.firstChild); 25 | } else if (e.target.className.indexOf('js-unzoom') >= 0) { 26 | unzoom(); 27 | } else { 28 | return; 29 | } 30 | 31 | e.preventDefault(); 32 | }, { capture: true }); 33 | 34 | const TAB = 9; 35 | const ESC = 27; 36 | document.body.addEventListener('keydown', e => { 37 | if (document.body.className === 'zoomed') { 38 | if (e.which === TAB) { 39 | document.getElementById('zoomedImage').parentNode.focus(); 40 | } else if (e.which === ESC) { 41 | unzoom(); 42 | } else { 43 | return; 44 | } 45 | 46 | e.preventDefault(); 47 | } 48 | }); 49 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "micro-gallery", 3 | "version": "0.4.5", 4 | "description": "Like zeit's micro-list, but for images.", 5 | "bin": { 6 | "gallery": "./dist/bin/gallery.js" 7 | }, 8 | "scripts": { 9 | "dev": "gallery", 10 | "build": "gulp build", 11 | "start": "gulp" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+ssh://git@github.com/andreasmcdermott/micro-gallery.git" 16 | }, 17 | "keywords": ["gallery", "images", "micro", "micro-list"], 18 | "author": "Andreas McDermott ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/andreasmcdermott/micro-gallery/issues" 22 | }, 23 | "homepage": "https://github.com/andreasmcdermott/micro-gallery#readme", 24 | "xo": { 25 | "semicolon": false, 26 | "space": true, 27 | "esnext": true 28 | }, 29 | "babel": { 30 | "plugins": [ 31 | "transform-es2015-modules-commonjs", 32 | "transform-async-to-generator" 33 | ] 34 | }, 35 | "dependencies": { 36 | "chalk": "^1.1.3", 37 | "denodeify": "^1.2.1", 38 | "handlebars": "^4.0.5", 39 | "micro": "^6.0.2", 40 | "mime": "^1.3.4", 41 | "minimist": "^1.2.0", 42 | "url": "^0.11.0" 43 | }, 44 | "devDependencies": { 45 | "babel-plugin-transform-async-to-generator": "^6.8.0", 46 | "babel-plugin-transform-es2015-modules-commonjs": "^6.14.0", 47 | "gulp": "^3.9.1", 48 | "gulp-babel": "^6.1.2", 49 | "gulp-cached": "^1.1.0", 50 | "gulp-chmod": "^1.3.0", 51 | "gulp-stylus": "^2.5.0", 52 | "prettier": "^1.5.3", 53 | "xo": "^0.16.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /assets/style.styl: -------------------------------------------------------------------------------- 1 | body 2 | background #fff 3 | margin 0 4 | padding 30px 5 | -webkit-font-smoothing antialiased 6 | font-family Menlo 7 | 8 | @media (min-width: 992px) 9 | padding 45px 10 | 11 | a 12 | color #1A00F2 13 | text-decoration none 14 | 15 | &:focus 16 | outline none 17 | border 1px dotted black 18 | margin -1px 19 | 20 | h1 21 | font-size 18px 22 | font-weight 500 23 | margin-top 0 24 | color #000 25 | font-family -apple-system, Helvetica 26 | display flex 27 | 28 | i 29 | font-style normal 30 | 31 | a 32 | color inherit 33 | font-weight bold 34 | border-bottom 1px dashed transparent 35 | 36 | &:after 37 | content '/' 38 | 39 | &:hover 40 | color #7d7d7d 41 | 42 | @media (min-width: 992px) 43 | font-size 15px 44 | 45 | main 46 | //max-width 920px 47 | 48 | ul 49 | margin 0 50 | padding 20px 0 0 0 51 | 52 | li 53 | list-style none 54 | padding 0 55 | font-size 14px 56 | display flex 57 | justify-content space-between 58 | 59 | i 60 | color #9B9B9B 61 | font-size 11px 62 | display block 63 | font-style normal 64 | 65 | @media (min-width: 992px) 66 | font-size 13px 67 | box-sizing border-box 68 | justify-content flex-start 69 | 70 | &:hover i 71 | opacity 1 72 | 73 | i 74 | font-size 10px 75 | opacity 0 76 | margin-left 10px 77 | margin-top 3px 78 | 79 | @media (min-width: 768px) 80 | display flex 81 | flex-wrap wrap 82 | 83 | .directories 84 | li 85 | @media (min-width: 768px) 86 | width 230px 87 | padding 10px 20px 10px 0 88 | 89 | a 90 | color #1A00F2 91 | white-space nowrap 92 | overflow hidden 93 | display block 94 | 95 | &:before 96 | content '\f114' 97 | font-family FontAwesome 98 | display inline-block 99 | margin-right 10px 100 | color #000 101 | 102 | &:hover 103 | color #000 104 | 105 | .images 106 | li 107 | @media (min-width: 768px) 108 | height 230px 109 | padding-right 20px 110 | margin-bottom: 10px 111 | 112 | img 113 | height 230px 114 | 115 | .image--zoomed 116 | display none 117 | position fixed 118 | top 0 119 | left 0 120 | width 100% 121 | height 100% 122 | padding 0 123 | margin 0 124 | background rgba(255, 255, 255, 0.7) 125 | 126 | img 127 | width auto 128 | height auto 129 | max-width 90% 130 | max-height 90% 131 | box-shadow 4px 4px 8px rgba(0, 0, 0, 0.5) 132 | 133 | .zoomed 134 | overflow hidden 135 | 136 | .image--zoomed 137 | display block 138 | -------------------------------------------------------------------------------- /dist/assets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #fff; 3 | margin: 0; 4 | padding: 30px; 5 | -webkit-font-smoothing: antialiased; 6 | font-family: Menlo; 7 | } 8 | @media (min-width: 992px) { 9 | body { 10 | padding: 45px; 11 | } 12 | } 13 | a { 14 | color: #1a00f2; 15 | text-decoration: none; 16 | } 17 | a:focus { 18 | outline: none; 19 | border: 1px dotted #000; 20 | margin: -1px; 21 | } 22 | h1 { 23 | font-size: 18px; 24 | font-weight: 500; 25 | margin-top: 0; 26 | color: #000; 27 | font-family: -apple-system, Helvetica; 28 | display: flex; 29 | } 30 | h1 i { 31 | font-style: normal; 32 | } 33 | h1 a { 34 | color: inherit; 35 | font-weight: bold; 36 | border-bottom: 1px dashed transparent; 37 | } 38 | h1 a:after { 39 | content: '/'; 40 | } 41 | h1 a:hover { 42 | color: #7d7d7d; 43 | } 44 | @media (min-width: 992px) { 45 | h1 { 46 | font-size: 15px; 47 | } 48 | } 49 | ul { 50 | margin: 0; 51 | padding: 20px 0 0 0; 52 | } 53 | ul li { 54 | list-style: none; 55 | padding: 0; 56 | font-size: 14px; 57 | display: flex; 58 | justify-content: space-between; 59 | } 60 | ul li i { 61 | color: #9b9b9b; 62 | font-size: 11px; 63 | display: block; 64 | font-style: normal; 65 | } 66 | @media (min-width: 992px) { 67 | ul li { 68 | font-size: 13px; 69 | box-sizing: border-box; 70 | justify-content: flex-start; 71 | } 72 | ul li:hover i { 73 | opacity: 1; 74 | } 75 | ul li i { 76 | font-size: 10px; 77 | opacity: 0; 78 | margin-left: 10px; 79 | margin-top: 3px; 80 | } 81 | } 82 | @media (min-width: 768px) { 83 | ul { 84 | display: flex; 85 | flex-wrap: wrap; 86 | } 87 | } 88 | @media (min-width: 768px) { 89 | .directories li { 90 | width: 230px; 91 | padding: 10px 20px 10px 0; 92 | } 93 | } 94 | .directories a { 95 | color: #1a00f2; 96 | white-space: nowrap; 97 | overflow: hidden; 98 | display: block; 99 | } 100 | .directories a:before { 101 | content: '\f114'; 102 | font-family: FontAwesome; 103 | display: inline-block; 104 | margin-right: 10px; 105 | color: #000; 106 | } 107 | .directories a:hover { 108 | color: #000; 109 | } 110 | @media (min-width: 768px) { 111 | .images li { 112 | height: 230px; 113 | padding-right: 20px; 114 | margin-bottom: 10px; 115 | } 116 | } 117 | .images li img { 118 | height: 230px; 119 | } 120 | .image--zoomed { 121 | display: none; 122 | position: fixed; 123 | top: 0; 124 | left: 0; 125 | width: 100%; 126 | height: 100%; 127 | padding: 0; 128 | margin: 0; 129 | background: rgba(255,255,255,0.7); 130 | } 131 | .image--zoomed img { 132 | width: auto; 133 | height: auto; 134 | max-width: 90%; 135 | max-height: 90%; 136 | box-shadow: 4px 4px 8px rgba(0,0,0,0.5); 137 | } 138 | .zoomed { 139 | overflow: hidden; 140 | } 141 | .zoomed .image--zoomed { 142 | display: block; 143 | } 144 | -------------------------------------------------------------------------------- /bin/gallery.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | 4 | import fs from 'fs' 5 | import path from 'path' 6 | import minimist from 'minimist' 7 | import toPromise from 'denodeify' 8 | import micro, { send } from 'micro' 9 | import mime from 'mime' 10 | import { parse } from 'url' 11 | import { green, red } from 'chalk' 12 | import Handlebars from 'handlebars' 13 | 14 | const defaultPort = 3000 15 | const validExtensions = new Set([ 16 | '.jpeg', 17 | '.jpg', 18 | '.png', 19 | '.gif', 20 | '.bmp', 21 | '.tiff', 22 | '.tif' 23 | ]) 24 | const ignoredFiles = new Set(['.git', '.DS_Store']) 25 | 26 | const argv = minimist(process.argv.slice(2), { 27 | alias: { 28 | dev: ['d'], 29 | port: ['p'] 30 | } 31 | }) 32 | 33 | const root = 34 | argv._.length > 0 ? path.resolve(process.cwd(), argv._[0]) : process.cwd() 35 | const rootObj = path.parse(root) 36 | 37 | const isDirectory = async directory => { 38 | try { 39 | const stats = await toPromise(fs.stat)(directory) 40 | return stats.isDirectory() 41 | } catch (err) { 42 | return false 43 | } 44 | } 45 | 46 | const exists = async filePath => { 47 | try { 48 | await toPromise(fs.stat)(filePath) 49 | return true 50 | } catch (err) { 51 | return false 52 | } 53 | } 54 | 55 | let cachedView = null 56 | const getView = async () => { 57 | if (!cachedView || argv.dev) { 58 | try { 59 | let file = await toPromise(fs.readFile)( 60 | path.resolve(__dirname, '../../views/index.hbs'), 61 | 'utf8' 62 | ) 63 | cachedView = Handlebars.compile(file) 64 | } catch (err) { 65 | throw err 66 | } 67 | } 68 | 69 | return cachedView 70 | } 71 | 72 | let cachedAssets = {} 73 | const getAsset = async assetPath => { 74 | if (!cachedAssets[assetPath] || argv.dev) { 75 | try { 76 | let file = await toPromise(fs.readFile)( 77 | path.resolve(__dirname, '../../dist/assets', assetPath), 78 | 'utf8' 79 | ) 80 | cachedAssets[assetPath] = file 81 | } catch (err) { 82 | throw err 83 | } 84 | } 85 | 86 | return cachedAssets[assetPath] 87 | } 88 | 89 | const renderDir = async directory => { 90 | const files = await toPromise(fs.readdir)(directory) 91 | const dirObj = path.parse(directory) 92 | let dirPath = `${dirObj.dir}/${dirObj.base}`.replace(`${rootObj.dir}/`, ``) 93 | let dirPathParts = dirPath.split('/') 94 | 95 | const data = { 96 | directories: [], 97 | images: [], 98 | path: [], 99 | assetsDir: '/assets', 100 | folder: dirObj.name 101 | } 102 | 103 | let url = [] 104 | for (let i = 0; i < dirPathParts.length; ++i) { 105 | if (dirPathParts[i] !== rootObj.base) { 106 | url.push(dirPathParts[i]) 107 | } 108 | 109 | data.path.push({ 110 | url: url.join('/'), 111 | name: dirPathParts[i] 112 | }) 113 | } 114 | 115 | for (let i = 0; i < files.length; ++i) { 116 | if (ignoredFiles.has(files[i])) { 117 | continue 118 | } 119 | 120 | const filePath = path.resolve(root, path.resolve(directory, files[i])) 121 | const relativeFilePath = path.relative( 122 | root, 123 | path.resolve(directory, files[i]) 124 | ) 125 | if (await isDirectory(filePath)) { 126 | data.directories.push({ 127 | relative: relativeFilePath, 128 | name: files[i] 129 | }) 130 | } else if (validExtensions.has(path.parse(filePath).ext)) { 131 | data.images.push({ 132 | relative: relativeFilePath, 133 | name: files[i] 134 | }) 135 | } 136 | } 137 | 138 | let view = await getView() 139 | return view(data) 140 | } 141 | 142 | const renderImage = async file => { 143 | try { 144 | const content = await toPromise(fs.readFile)(path.resolve(root, file)) 145 | return { 146 | content: content, 147 | mime: mime.lookup(file) 148 | } 149 | } catch (err) { 150 | throw err 151 | } 152 | } 153 | 154 | const server = micro(async (req, res) => { 155 | const { pathname } = parse(req.url) 156 | const pathObj = path.parse(path.join(root, pathname)) 157 | const reqPath = decodeURIComponent(path.format(pathObj)) 158 | 159 | if (pathname.startsWith('/assets')) { 160 | let asset = await getAsset(pathname.replace('/assets/', '')) 161 | res.setHeader('Content-Type', `${mime.lookup(pathname)}; charset=utf-8`) 162 | return send(res, 200, asset) 163 | } 164 | 165 | if (!await exists(reqPath)) { 166 | return send(res, 404, 'Not found') 167 | } 168 | 169 | if (pathObj.ext === '') { 170 | const renderedDir = await renderDir(reqPath) 171 | return send(res, 200, renderedDir) 172 | } else if (validExtensions.has(pathObj.ext)) { 173 | try { 174 | const image = await renderImage(reqPath) 175 | res.setHeader('Content-Type', `${image.mime}; charset=utf-8`) 176 | return send(res, 200, image.content) 177 | } catch (err) { 178 | return send(res, 500, 'Error reading file content') 179 | } 180 | } else { 181 | return send(res, 400, 'Bad request') 182 | } 183 | }) 184 | 185 | server.listen(argv.port || defaultPort, async () => { 186 | process.on('SIGINT', () => { 187 | server.close() 188 | process.exit(0) 189 | }) 190 | 191 | if (!await isDirectory(root)) { 192 | console.error(red(`Specified directory doesn't exist!`)) 193 | process.exit(1) 194 | } 195 | 196 | console.log(green(`Running on http://localhost:${server.address().port}`)) 197 | }) 198 | -------------------------------------------------------------------------------- /dist/bin/gallery.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var _fs = require('fs'); 6 | 7 | var _fs2 = _interopRequireDefault(_fs); 8 | 9 | var _path = require('path'); 10 | 11 | var _path2 = _interopRequireDefault(_path); 12 | 13 | var _minimist = require('minimist'); 14 | 15 | var _minimist2 = _interopRequireDefault(_minimist); 16 | 17 | var _denodeify = require('denodeify'); 18 | 19 | var _denodeify2 = _interopRequireDefault(_denodeify); 20 | 21 | var _micro = require('micro'); 22 | 23 | var _micro2 = _interopRequireDefault(_micro); 24 | 25 | var _mime = require('mime'); 26 | 27 | var _mime2 = _interopRequireDefault(_mime); 28 | 29 | var _url = require('url'); 30 | 31 | var _chalk = require('chalk'); 32 | 33 | var _handlebars = require('handlebars'); 34 | 35 | var _handlebars2 = _interopRequireDefault(_handlebars); 36 | 37 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 38 | 39 | function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { return step("next", value); }, function (err) { return step("throw", err); }); } } return step("next"); }); }; } 40 | 41 | const defaultPort = 3000; 42 | const validExtensions = new Set(['.jpeg', '.jpg', '.png', '.gif', '.bmp', '.tiff', '.tif']); 43 | const ignoredFiles = new Set(['.git', '.DS_Store']); 44 | 45 | const argv = (0, _minimist2.default)(process.argv.slice(2), { 46 | alias: { 47 | dev: ['d'], 48 | port: ['p'] 49 | } 50 | }); 51 | 52 | const root = argv._.length > 0 ? _path2.default.resolve(process.cwd(), argv._[0]) : process.cwd(); 53 | const rootObj = _path2.default.parse(root); 54 | 55 | const isDirectory = (() => { 56 | var _ref = _asyncToGenerator(function* (directory) { 57 | try { 58 | const stats = yield (0, _denodeify2.default)(_fs2.default.stat)(directory); 59 | return stats.isDirectory(); 60 | } catch (err) { 61 | return false; 62 | } 63 | }); 64 | 65 | return function isDirectory(_x) { 66 | return _ref.apply(this, arguments); 67 | }; 68 | })(); 69 | 70 | const exists = (() => { 71 | var _ref2 = _asyncToGenerator(function* (filePath) { 72 | try { 73 | yield (0, _denodeify2.default)(_fs2.default.stat)(filePath); 74 | return true; 75 | } catch (err) { 76 | return false; 77 | } 78 | }); 79 | 80 | return function exists(_x2) { 81 | return _ref2.apply(this, arguments); 82 | }; 83 | })(); 84 | 85 | let cachedView = null; 86 | const getView = (() => { 87 | var _ref3 = _asyncToGenerator(function* () { 88 | if (!cachedView || argv.dev) { 89 | try { 90 | let file = yield (0, _denodeify2.default)(_fs2.default.readFile)(_path2.default.resolve(__dirname, '../../views/index.hbs'), 'utf8'); 91 | cachedView = _handlebars2.default.compile(file); 92 | } catch (err) { 93 | throw err; 94 | } 95 | } 96 | 97 | return cachedView; 98 | }); 99 | 100 | return function getView() { 101 | return _ref3.apply(this, arguments); 102 | }; 103 | })(); 104 | 105 | let cachedAssets = {}; 106 | const getAsset = (() => { 107 | var _ref4 = _asyncToGenerator(function* (assetPath) { 108 | if (!cachedAssets[assetPath] || argv.dev) { 109 | try { 110 | let file = yield (0, _denodeify2.default)(_fs2.default.readFile)(_path2.default.resolve(__dirname, '../../dist/assets', assetPath), 'utf8'); 111 | cachedAssets[assetPath] = file; 112 | } catch (err) { 113 | throw err; 114 | } 115 | } 116 | 117 | return cachedAssets[assetPath]; 118 | }); 119 | 120 | return function getAsset(_x3) { 121 | return _ref4.apply(this, arguments); 122 | }; 123 | })(); 124 | 125 | const renderDir = (() => { 126 | var _ref5 = _asyncToGenerator(function* (directory) { 127 | const files = yield (0, _denodeify2.default)(_fs2.default.readdir)(directory); 128 | const dirObj = _path2.default.parse(directory); 129 | let dirPath = `${ dirObj.dir }/${ dirObj.base }`.replace(`${ rootObj.dir }/`, ``); 130 | let dirPathParts = dirPath.split('/'); 131 | 132 | const data = { 133 | directories: [], 134 | images: [], 135 | path: [], 136 | assetsDir: '/assets', 137 | folder: dirObj.name 138 | }; 139 | 140 | let url = []; 141 | for (let i = 0; i < dirPathParts.length; ++i) { 142 | if (dirPathParts[i] !== rootObj.base) { 143 | url.push(dirPathParts[i]); 144 | } 145 | 146 | data.path.push({ 147 | url: url.join('/'), 148 | name: dirPathParts[i] 149 | }); 150 | } 151 | 152 | for (let i = 0; i < files.length; ++i) { 153 | if (ignoredFiles.has(files[i])) { 154 | continue; 155 | } 156 | 157 | const filePath = _path2.default.resolve(root, _path2.default.resolve(directory, files[i])); 158 | const relativeFilePath = _path2.default.relative(root, _path2.default.resolve(directory, files[i])); 159 | if (yield isDirectory(filePath)) { 160 | data.directories.push({ 161 | relative: relativeFilePath, 162 | name: files[i] 163 | }); 164 | } else if (validExtensions.has(_path2.default.parse(filePath).ext)) { 165 | data.images.push({ 166 | relative: relativeFilePath, 167 | name: files[i] 168 | }); 169 | } 170 | } 171 | 172 | let view = yield getView(); 173 | return view(data); 174 | }); 175 | 176 | return function renderDir(_x4) { 177 | return _ref5.apply(this, arguments); 178 | }; 179 | })(); 180 | 181 | const renderImage = (() => { 182 | var _ref6 = _asyncToGenerator(function* (file) { 183 | try { 184 | const content = yield (0, _denodeify2.default)(_fs2.default.readFile)(_path2.default.resolve(root, file)); 185 | return { 186 | content: content, 187 | mime: _mime2.default.lookup(file) 188 | }; 189 | } catch (err) { 190 | throw err; 191 | } 192 | }); 193 | 194 | return function renderImage(_x5) { 195 | return _ref6.apply(this, arguments); 196 | }; 197 | })(); 198 | 199 | const server = (0, _micro2.default)((() => { 200 | var _ref7 = _asyncToGenerator(function* (req, res) { 201 | const { pathname } = (0, _url.parse)(req.url); 202 | const pathObj = _path2.default.parse(_path2.default.join(root, pathname)); 203 | const reqPath = decodeURIComponent(_path2.default.format(pathObj)); 204 | 205 | if (pathname.startsWith('/assets')) { 206 | let asset = yield getAsset(pathname.replace('/assets/', '')); 207 | res.setHeader('Content-Type', `${ _mime2.default.lookup(pathname) }; charset=utf-8`); 208 | return (0, _micro.send)(res, 200, asset); 209 | } 210 | 211 | if (!(yield exists(reqPath))) { 212 | return (0, _micro.send)(res, 404, 'Not found'); 213 | } 214 | 215 | if (pathObj.ext === '') { 216 | const renderedDir = yield renderDir(reqPath); 217 | return (0, _micro.send)(res, 200, renderedDir); 218 | } else if (validExtensions.has(pathObj.ext)) { 219 | try { 220 | const image = yield renderImage(reqPath); 221 | res.setHeader('Content-Type', `${ image.mime }; charset=utf-8`); 222 | return (0, _micro.send)(res, 200, image.content); 223 | } catch (err) { 224 | return (0, _micro.send)(res, 500, 'Error reading file content'); 225 | } 226 | } else { 227 | return (0, _micro.send)(res, 400, 'Bad request'); 228 | } 229 | }); 230 | 231 | return function (_x6, _x7) { 232 | return _ref7.apply(this, arguments); 233 | }; 234 | })()); 235 | 236 | server.listen(argv.port || defaultPort, _asyncToGenerator(function* () { 237 | process.on('SIGINT', function () { 238 | server.close(); 239 | process.exit(0); 240 | }); 241 | 242 | if (!(yield isDirectory(root))) { 243 | console.error((0, _chalk.red)(`Specified directory doesn't exist!`)); 244 | process.exit(1); 245 | } 246 | 247 | console.log((0, _chalk.green)(`Running on http://localhost:${ server.address().port }`)); 248 | })); --------------------------------------------------------------------------------