diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..f5b97a8 --- /dev/null +++ b/index.js @@ -0,0 +1,44 @@ +const Koa = require('koa'); +const Router = require('koa-router'); +const redisCli = require('./util/client'); +const db = require('./util/db'); + +const app = new Koa(); +const router = new Router(); + +const PORT = 3000; +const CACHE_KEY = 'cache-top5'; + +router.get('/', async (ctx, next) => { + await next(); + redisCli.set('testcli', '我只是想看看封装的能不能用着了', 'EX', 3600); + ctx.body = `It's OK! ${PORT}` +}); + +router.get('/cache', async (ctx, next) => { + await next(); + + let top5 = await redisCli.get(CACHE_KEY); + if(!top5) { + top5 = await db.query('select * from websites limit 5'); + console.log('get db: ', top5.length); + if(await redisCli.checkLock(CACHE_KEY)) { + ctx.body = top5; + } + try { + await redisCli.set(CACHE_KEY, JSON.stringify(top5), 'PX', 360000); + } catch (err) { + console.log(`redisCli set err: ${err}`); + ctx.body = top5; + } + } + + console.log('get cache: ', top5.length); + ctx.body = top5; +}); + +app.use(router.routes()).use(router.allowedMethods()); + +app.listen(PORT, () => { + console.log(`listening http://localhost:${PORT}`); +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0b6c0dd --- /dev/null +++ b/package-lock.json @@ -0,0 +1,423 @@ +{ + "name": "homework-cache", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npm.taobao.org/accepts/download/accepts-1.3.7.tgz", + "integrity": "sha1-UxvHJlF6OytB+FACHGzBXqq1B80=", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npm.taobao.org/any-promise/download/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + }, + "bignumber.js": { + "version": "7.2.1", + "resolved": "https://registry.npm.taobao.org/bignumber.js/download/bignumber.js-7.2.1.tgz", + "integrity": "sha1-gMBIdZ2CaACAfEv9Uh5Q7bulel8=" + }, + "cache-content-type": { + "version": "1.0.1", + "resolved": "https://registry.npm.taobao.org/cache-content-type/download/cache-content-type-1.0.1.tgz", + "integrity": "sha1-A1zeKwjuISn0qDFeqPAKANuhRTw=", + "requires": { + "mime-types": "^2.1.18", + "ylru": "^1.2.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "http://registry.npm.taobao.org/co/download/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "http://registry.npm.taobao.org/content-disposition/download/content-disposition-0.5.3.tgz", + "integrity": "sha1-4TDK9+cnkIfFYWwgB9BIVpiYT70=", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "http://registry.npm.taobao.org/content-type/download/content-type-1.0.4.tgz", + "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=" + }, + "cookies": { + "version": "0.7.3", + "resolved": "https://registry.npm.taobao.org/cookies/download/cookies-0.7.3.tgz", + "integrity": "sha1-eRLOIfvy6MLacM8cPzUa7PWdrfo=", + "requires": { + "depd": "~1.1.2", + "keygrip": "~1.0.3" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "http://registry.npm.taobao.org/core-util-is/download/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "debug": { + "version": "3.1.0", + "resolved": "http://registry.npm.taobao.org/debug/download/debug-3.1.0.tgz", + "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", + "requires": { + "ms": "2.0.0" + } + }, + "deep-equal": { + "version": "1.0.1", + "resolved": "http://registry.npm.taobao.org/deep-equal/download/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" + }, + "delegates": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/delegates/download/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "depd": { + "version": "1.1.2", + "resolved": "http://registry.npm.taobao.org/depd/download/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "http://registry.npm.taobao.org/destroy/download/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "double-ended-queue": { + "version": "2.1.0-0", + "resolved": "https://registry.npm.taobao.org/double-ended-queue/download/double-ended-queue-2.1.0-0.tgz", + "integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw=" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "http://registry.npm.taobao.org/ee-first/download/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "error-inject": { + "version": "1.0.0", + "resolved": "https://registry.npm.taobao.org/error-inject/download/error-inject-1.0.0.tgz", + "integrity": "sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "http://registry.npm.taobao.org/escape-html/download/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "http://registry.npm.taobao.org/fresh/download/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "http-assert": { + "version": "1.4.1", + "resolved": "https://registry.npm.taobao.org/http-assert/download/http-assert-1.4.1.tgz", + "integrity": "sha1-xfcl1neqfoc+9zYZm4lobM6zeHg=", + "requires": { + "deep-equal": "~1.0.1", + "http-errors": "~1.7.2" + } + }, + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npm.taobao.org/http-errors/download/http-errors-1.7.3.tgz", + "integrity": "sha1-bGGeT5xgMIw4UZSYwU+7EKrOuwY=", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npm.taobao.org/inherits/download/inherits-2.0.4.tgz", + "integrity": "sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w=" + }, + "is-generator-function": { + "version": "1.0.7", + "resolved": "https://registry.npm.taobao.org/is-generator-function/download/is-generator-function-1.0.7.tgz", + "integrity": "sha1-0hMuUpuwAAp/gHlNS99c1eWBNSI=" + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npm.taobao.org/isarray/download/isarray-0.0.1.tgz?cache=0&sync_timestamp=1562592096220&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fisarray%2Fdownload%2Fisarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "keygrip": { + "version": "1.0.3", + "resolved": "https://registry.npm.taobao.org/keygrip/download/keygrip-1.0.3.tgz", + "integrity": "sha1-OZ1wnwrtK6sKBZ4M3TpQI6BT4dw=" + }, + "koa": { + "version": "2.7.0", + "resolved": "https://registry.npm.taobao.org/koa/download/koa-2.7.0.tgz", + "integrity": "sha1-fgCENQaUK52CxswzdJ9lfG5eet8=", + "requires": { + "accepts": "^1.3.5", + "cache-content-type": "^1.0.0", + "content-disposition": "~0.5.2", + "content-type": "^1.0.4", + "cookies": "~0.7.1", + "debug": "~3.1.0", + "delegates": "^1.0.0", + "depd": "^1.1.2", + "destroy": "^1.0.4", + "error-inject": "^1.0.0", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.3.0", + "http-errors": "^1.6.3", + "is-generator-function": "^1.0.7", + "koa-compose": "^4.1.0", + "koa-convert": "^1.2.0", + "koa-is-json": "^1.0.0", + "on-finished": "^2.3.0", + "only": "~0.0.2", + "parseurl": "^1.3.2", + "statuses": "^1.5.0", + "type-is": "^1.6.16", + "vary": "^1.1.2" + } + }, + "koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npm.taobao.org/koa-compose/download/koa-compose-4.1.0.tgz", + "integrity": "sha1-UHMGuTcZAdtBEhyBLpI9DWfT6Hc=" + }, + "koa-convert": { + "version": "1.2.0", + "resolved": "https://registry.npm.taobao.org/koa-convert/download/koa-convert-1.2.0.tgz", + "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", + "requires": { + "co": "^4.6.0", + "koa-compose": "^3.0.0" + }, + "dependencies": { + "koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npm.taobao.org/koa-compose/download/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", + "requires": { + "any-promise": "^1.1.0" + } + } + } + }, + "koa-is-json": { + "version": "1.0.0", + "resolved": "https://registry.npm.taobao.org/koa-is-json/download/koa-is-json-1.0.0.tgz", + "integrity": "sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=" + }, + "koa-router": { + "version": "7.4.0", + "resolved": "https://registry.npm.taobao.org/koa-router/download/koa-router-7.4.0.tgz", + "integrity": "sha1-ruH3rcAtXLMdfWdGXJ6syCXoxeA=", + "requires": { + "debug": "^3.1.0", + "http-errors": "^1.3.1", + "koa-compose": "^3.0.0", + "methods": "^1.0.1", + "path-to-regexp": "^1.1.1", + "urijs": "^1.19.0" + }, + "dependencies": { + "koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npm.taobao.org/koa-compose/download/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", + "requires": { + "any-promise": "^1.1.0" + } + } + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npm.taobao.org/media-typer/download/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "methods": { + "version": "1.1.2", + "resolved": "http://registry.npm.taobao.org/methods/download/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime-db": { + "version": "1.40.0", + "resolved": "http://registry.npm.taobao.org/mime-db/download/mime-db-1.40.0.tgz", + "integrity": "sha1-plBX6ZjbCQ9zKmj2wnbTh9QSbDI=" + }, + "mime-types": { + "version": "2.1.24", + "resolved": "http://registry.npm.taobao.org/mime-types/download/mime-types-2.1.24.tgz", + "integrity": "sha1-tvjQs+lR77d97eyhlM/20W9nb4E=", + "requires": { + "mime-db": "1.40.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npm.taobao.org/ms/download/ms-2.0.0.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fms%2Fdownload%2Fms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "mysql": { + "version": "2.17.1", + "resolved": "https://registry.npm.taobao.org/mysql/download/mysql-2.17.1.tgz", + "integrity": "sha1-YrukoDmpsvc2OM0WUs5Q/G9oKJk=", + "requires": { + "bignumber.js": "7.2.1", + "readable-stream": "2.3.6", + "safe-buffer": "5.1.2", + "sqlstring": "2.3.1" + } + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npm.taobao.org/negotiator/download/negotiator-0.6.2.tgz", + "integrity": "sha1-/qz3zPUlp3rpY0Q2pkiD/+yjRvs=" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "http://registry.npm.taobao.org/on-finished/download/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "only": { + "version": "0.0.2", + "resolved": "https://registry.npm.taobao.org/only/download/only-0.0.2.tgz", + "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=" + }, + "parseurl": { + "version": "1.3.3", + "resolved": "http://registry.npm.taobao.org/parseurl/download/parseurl-1.3.3.tgz", + "integrity": "sha1-naGee+6NEt/wUT7Vt2lXeTvC6NQ=" + }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "http://registry.npm.taobao.org/path-to-regexp/download/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "requires": { + "isarray": "0.0.1" + } + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npm.taobao.org/process-nextick-args/download/process-nextick-args-2.0.1.tgz", + "integrity": "sha1-eCDZsWEgzFXKmud5JoCufbptf+I=" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npm.taobao.org/readable-stream/download/readable-stream-2.3.6.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Freadable-stream%2Fdownload%2Freadable-stream-2.3.6.tgz", + "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npm.taobao.org/isarray/download/isarray-1.0.0.tgz?cache=0&sync_timestamp=1562592096220&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fisarray%2Fdownload%2Fisarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + } + } + }, + "redis": { + "version": "2.8.0", + "resolved": "https://registry.npm.taobao.org/redis/download/redis-2.8.0.tgz", + "integrity": "sha1-ICKI4/WMSfYHnZevehDhMDrhSwI=", + "requires": { + "double-ended-queue": "^2.1.0-0", + "redis-commands": "^1.2.0", + "redis-parser": "^2.6.0" + } + }, + "redis-commands": { + "version": "1.5.0", + "resolved": "https://registry.npm.taobao.org/redis-commands/download/redis-commands-1.5.0.tgz", + "integrity": "sha1-gNLiBpj+aI8icSf/nlFkp90X54U=" + }, + "redis-parser": { + "version": "2.6.0", + "resolved": "https://registry.npm.taobao.org/redis-parser/download/redis-parser-2.6.0.tgz", + "integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs=" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npm.taobao.org/safe-buffer/download/safe-buffer-5.1.2.tgz", + "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=" + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npm.taobao.org/setprototypeof/download/setprototypeof-1.1.1.tgz", + "integrity": "sha1-fpWsskqpL1iF4KvvW6ExMw1K5oM=" + }, + "sqlstring": { + "version": "2.3.1", + "resolved": "https://registry.npm.taobao.org/sqlstring/download/sqlstring-2.3.1.tgz", + "integrity": "sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A=" + }, + "statuses": { + "version": "1.5.0", + "resolved": "http://registry.npm.taobao.org/statuses/download/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npm.taobao.org/string_decoder/download/string_decoder-1.1.1.tgz?cache=0&sync_timestamp=1565170823020&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fstring_decoder%2Fdownload%2Fstring_decoder-1.1.1.tgz", + "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/toidentifier/download/toidentifier-1.0.0.tgz", + "integrity": "sha1-fhvjRw8ed5SLxD2Uo8j013UrpVM=" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npm.taobao.org/type-is/download/type-is-1.6.18.tgz", + "integrity": "sha1-TlUs0F3wlGfcvE73Od6J8s83wTE=", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "urijs": { + "version": "1.19.1", + "resolved": "https://registry.npm.taobao.org/urijs/download/urijs-1.19.1.tgz", + "integrity": "sha1-Ww/1MMDL3oOG9jQiNbpcpumV0lo=" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "http://registry.npm.taobao.org/util-deprecate/download/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "vary": { + "version": "1.1.2", + "resolved": "http://registry.npm.taobao.org/vary/download/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "ylru": { + "version": "1.2.1", + "resolved": "https://registry.npm.taobao.org/ylru/download/ylru-1.2.1.tgz", + "integrity": "sha1-9Xa2M0FUeYnB3nuiiHYJI7J/6E8=" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3fe0552 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "homework-cache", + "version": "1.0.0", + "description": "redis homework-cache.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/xiaoyexiang/homework-cache.git" + }, + "keywords": [ + "redis", + "mysql", + "cache" + ], + "author": "yezi", + "license": "ISC", + "bugs": { + "url": "https://github.com/xiaoyexiang/homework-cache/issues" + }, + "homepage": "https://github.com/xiaoyexiang/homework-cache#readme", + "dependencies": { + "koa": "^2.7.0", + "koa-router": "^7.4.0", + "mysql": "^2.17.1", + "redis": "^2.8.0" + } +} diff --git a/util/client.js b/util/client.js new file mode 100644 index 0000000..f7733e8 --- /dev/null +++ b/util/client.js @@ -0,0 +1,93 @@ +//引入redis +const redis = require('redis'); + +// 连接redis服务器 +client = redis.createClient({ + host: '127.0.0.1', + port: 6379, +}); + +//错误监听? +client.on('error', function (err) { + console.log('error: ', err); +}); + +client.on('connect', function() { + console.log('Redis 连接成功!') +}); + +const REDIS_LOCK_KEY = 'cache-lock:'; + +const redisSet = async (key, value, exp, time) => { + const lockKey = REDIS_LOCK_KEY + key; + await redisLock(lockKey); + return new Promise((resolve, reject) => { + client.set(key, value, exp, time, async (err, result) => { + console.log('---------------- redisSet -----------------'); + await redisUnlock(lockKey); + err ? reject(err) : resolve(result); + }) + }) +}; + +const redisGet = key => { + return new Promise((resolve, reject) => { + client.get(key, (err, result) => { + console.log('---------------- redisGet -----------------'); + if(err) reject(err); + else { + try { + result = JSON.parse(result); + resolve(result); + } catch (err) { + console.log('err: ', err); + resolve(result); + } + } + }) + }) +}; + +// 锁成功返回1,已锁返回0 +const redisLock = (key) => { + return new Promise((resolve, reject) => { + console.log('---------------- redisLock -----------------'); + client.setnx(key, Date.now(), (err, islock) => { + if (err) reject(err); + else resolve(islock); + }) + }) +} + +const redisCheckLock = (key) => { + const lockKey = REDIS_LOCK_KEY + key; + + return new Promise((resolve, reject) => { + console.log('---------------- redisCheckLock -----------------'); + client.get(lockKey, (err, result) => { + if(err) reject(err); + else { + resolve(result); + } + }) + }) +} + +// 解锁成功返回1,已锁返回0 +const redisUnlock = (key) => { + return new Promise((resolve, reject) => { + console.log('---------------- redisUnlock -----------------'); + client.del(key, (err, data) => { + if (err) reject(err) + else resolve(data) + }); + }); +} + +module.exports = { + set: redisSet, + get: redisGet, + lock: redisLock, + checkLock: redisCheckLock, + unlock: redisUnlock, +}; \ No newline at end of file diff --git a/util/db.js b/util/db.js new file mode 100644 index 0000000..37d8d25 --- /dev/null +++ b/util/db.js @@ -0,0 +1,34 @@ +const mysql = require('mysql') + +const pool = mysql.createPool({ + host : '127.0.0.1', + user : 'root', + password : '1111122963', + database : 'test' +}) + +let query = function( sql, values ) { + + return new Promise(( resolve, reject ) => { + pool.getConnection(function(err, connection) { + if (err) { + reject( err ) + } else { + connection.query(sql, values, ( err, rows) => { + + if ( err ) { + reject( err ) + } else { + resolve( rows ) + } + connection.release() + }) + } + }) + }) + +} + +module.exports = { + query: query, +} \ No newline at end of file