Skip to content

Instantly share code, notes, and snippets.

@dunstontc
Last active June 8, 2017 00:06
Show Gist options
  • Save dunstontc/289527e0be180228c27b303d6edc2b0e to your computer and use it in GitHub Desktop.
Save dunstontc/289527e0be180228c27b303d6edc2b0e to your computer and use it in GitHub Desktop.
atom & the fs
/** @babel */
const fs = require('fs-plus')
const path = require('path')
const hasWriteAccess = (dir) => {
const testFilePath = path.join(dir, 'write.test')
try {
fs.writeFileSync(testFilePath, new Date().toISOString(), { flag: 'w+' })
fs.unlinkSync(testFilePath)
return true
} catch (err) {
return false
}
}
const getAppDirectory = () => {
switch (process.platform) {
case 'darwin':
return process.execPath.substring(0, process.execPath.indexOf('.app') + 4)
case 'linux':
case 'win32':
return path.join(process.execPath, '..')
}
}
module.exports = {
setAtomHome: (homePath) => {
// When a read-writeable .atom folder exists above app use that
const portableHomePath = path.join(getAppDirectory(), '..', '.atom')
if (fs.existsSync(portableHomePath)) {
if (hasWriteAccess(portableHomePath)) {
process.env.ATOM_HOME = portableHomePath
} else {
// A path exists so it was intended to be used but we didn't have rights, so warn.
console.log(`Insufficient permission to portable Atom home "${portableHomePath}".`)
}
}
// Check ATOM_HOME environment variable next
if (process.env.ATOM_HOME !== undefined) {
return
}
// Fall back to default .atom folder in users home folder
process.env.ATOM_HOME = path.join(homePath, '.atom')
},
setUserData: (app) => {
const electronUserDataPath = path.join(process.env.ATOM_HOME, 'electronUserData')
if (fs.existsSync(electronUserDataPath)) {
if (hasWriteAccess(electronUserDataPath)) {
app.setPath('userData', electronUserDataPath)
} else {
// A path exists so it was intended to be used but we didn't have rights, so warn.
console.log(`Insufficient permission to Electron user data "${electronUserDataPath}".`)
}
}
},
getAppDirectory: getAppDirectory
}
{Directory} = require 'pathwatcher'
fs = require 'fs-plus'
path = require 'path'
url = require 'url'
module.exports =
class DefaultDirectoryProvider
# Public: Create a Directory that corresponds to the specified URI.
#
# * `uri` {String} The path to the directory to add. This is guaranteed not to
# be contained by a {Directory} in `atom.project`.
#
# Returns:
# * {Directory} if the given URI is compatible with this provider.
# * `null` if the given URI is not compatibile with this provider.
directoryForURISync: (uri) ->
normalizedPath = @normalizePath(uri)
{host} = url.parse(uri)
directoryPath = if host
uri
else if not fs.isDirectorySync(normalizedPath) and fs.isDirectorySync(path.dirname(normalizedPath))
path.dirname(normalizedPath)
else
normalizedPath
# TODO: Stop normalizing the path in pathwatcher's Directory.
directory = new Directory(directoryPath)
if host
directory.path = directoryPath
if fs.isCaseInsensitive()
directory.lowerCasePath = directoryPath.toLowerCase()
directory
# Public: Create a Directory that corresponds to the specified URI.
#
# * `uri` {String} The path to the directory to add. This is guaranteed not to
# be contained by a {Directory} in `atom.project`.
#
# Returns a {Promise} that resolves to:
# * {Directory} if the given URI is compatible with this provider.
# * `null` if the given URI is not compatibile with this provider.
directoryForURI: (uri) ->
Promise.resolve(@directoryForURISync(uri))
# Public: Normalizes path.
#
# * `uri` {String} The path that should be normalized.
#
# Returns a {String} with normalized path.
normalizePath: (uri) ->
# Normalize disk drive letter on Windows to avoid opening two buffers for the same file
pathWithNormalizedDiskDriveLetter =
if process.platform is 'win32' and matchData = uri.match(/^([a-z]):/)
"#{matchData[1].toUpperCase()}#{uri.slice(1)}"
else
uri
path.normalize(pathWithNormalizedDiskDriveLetter)
Task = require './task'
# Searches local files for lines matching a specified regex. Implements `.then()`
# so that it can be used with `Promise.all()`.
class DirectorySearch
constructor: (rootPaths, regex, options) ->
scanHandlerOptions =
ignoreCase: regex.ignoreCase
inclusions: options.inclusions
includeHidden: options.includeHidden
excludeVcsIgnores: options.excludeVcsIgnores
globalExclusions: options.exclusions
follow: options.follow
searchOptions =
leadingContextLineCount: options.leadingContextLineCount
trailingContextLineCount: options.trailingContextLineCount
@task = new Task(require.resolve('./scan-handler'))
@task.on 'scan:result-found', options.didMatch
@task.on 'scan:file-error', options.didError
@task.on 'scan:paths-searched', options.didSearchPaths
@promise = new Promise (resolve, reject) =>
@task.on('task:cancelled', reject)
@task.start rootPaths, regex.source, scanHandlerOptions, searchOptions, =>
@task.terminate()
resolve()
then: (args...) ->
@promise.then.apply(@promise, args)
cancel: ->
# This will cause @promise to reject.
@task.cancel()
null
# Default provider for the `atom.directory-searcher` service.
module.exports =
class DefaultDirectorySearcher
# Determines whether this object supports search for a `Directory`.
#
# * `directory` {Directory} whose search needs might be supported by this object.
#
# Returns a `boolean` indicating whether this object can search this `Directory`.
canSearchDirectory: (directory) -> true
# Performs a text search for files in the specified `Directory`, subject to the
# specified parameters.
#
# Results are streamed back to the caller by invoking methods on the specified `options`,
# such as `didMatch` and `didError`.
#
# * `directories` {Array} of {Directory} objects to search, all of which have been accepted by
# this searcher's `canSearchDirectory()` predicate.
# * `regex` {RegExp} to search with.
# * `options` {Object} with the following properties:
# * `didMatch` {Function} call with a search result structured as follows:
# * `searchResult` {Object} with the following keys:
# * `filePath` {String} absolute path to the matching file.
# * `matches` {Array} with object elements with the following keys:
# * `lineText` {String} The full text of the matching line (without a line terminator character).
# * `lineTextOffset` {Number} (This always seems to be 0?)
# * `matchText` {String} The text that matched the `regex` used for the search.
# * `range` {Range} Identifies the matching region in the file. (Likely as an array of numeric arrays.)
# * `didError` {Function} call with an Error if there is a problem during the search.
# * `didSearchPaths` {Function} periodically call with the number of paths searched thus far.
# * `inclusions` {Array} of glob patterns (as strings) to search within. Note that this
# array may be empty, indicating that all files should be searched.
#
# Each item in the array is a file/directory pattern, e.g., `src` to search in the "src"
# directory or `*.js` to search all JavaScript files. In practice, this often comes from the
# comma-delimited list of patterns in the bottom text input of the ProjectFindView dialog.
# * `ignoreHidden` {boolean} whether to ignore hidden files.
# * `excludeVcsIgnores` {boolean} whether to exclude VCS ignored paths.
# * `exclusions` {Array} similar to inclusions
# * `follow` {boolean} whether symlinks should be followed.
#
# Returns a *thenable* `DirectorySearch` that includes a `cancel()` method. If `cancel()` is
# invoked before the `DirectorySearch` is determined, it will resolve the `DirectorySearch`.
search: (directories, regex, options) ->
rootPaths = directories.map (directory) -> directory.getPath()
isCancelled = false
directorySearch = new DirectorySearch(rootPaths, regex, options)
promise = new Promise (resolve, reject) ->
directorySearch.then resolve, ->
if isCancelled
resolve()
else
reject()
return {
then: promise.then.bind(promise)
catch: promise.catch.bind(promise)
cancel: ->
isCancelled = true
directorySearch.cancel()
}
'use strict'
const fs = require('fs-plus')
const path = require('path')
module.exports =
class FileSystemBlobStore {
static load(directory) {
let instance = new FileSystemBlobStore(directory)
instance.load()
return instance
}
constructor(directory) {
this.blobFilename = path.join(directory, 'BLOB')
this.blobMapFilename = path.join(directory, 'MAP')
this.lockFilename = path.join(directory, 'LOCK')
this.reset()
}
reset() {
this.inMemoryBlobs = new Map()
this.storedBlob = new Buffer(0)
this.storedBlobMap = {}
this.usedKeys = new Set()
}
load() {
if (!fs.existsSync(this.blobMapFilename)) {
return
}
if (!fs.existsSync(this.blobFilename)) {
return
}
try {
this.storedBlob = fs.readFileSync(this.blobFilename)
this.storedBlobMap = JSON.parse(fs.readFileSync(this.blobMapFilename))
} catch (e) {
this.reset()
}
}
save() {
let dump = this.getDump()
let blobToStore = Buffer.concat(dump[0])
let mapToStore = JSON.stringify(dump[1])
let acquiredLock = false
try {
fs.writeFileSync(this.lockFilename, 'LOCK', {
flag: 'wx'
})
acquiredLock = true
fs.writeFileSync(this.blobFilename, blobToStore)
fs.writeFileSync(this.blobMapFilename, mapToStore)
} catch (error) {
// Swallow the exception silently only if we fail to acquire the lock.
if (error.code !== 'EEXIST') {
throw error
}
} finally {
if (acquiredLock) {
fs.unlinkSync(this.lockFilename)
}
}
}
has(key) {
return this.inMemoryBlobs.has(key) || this.storedBlobMap.hasOwnProperty(key)
}
get(key) {
if (this.has(key)) {
this.usedKeys.add(key)
return this.getFromMemory(key) || this.getFromStorage(key)
}
}
set(key, buffer) {
this.usedKeys.add(key)
return this.inMemoryBlobs.set(key, buffer)
}
delete(key) {
this.inMemoryBlobs.delete(key)
delete this.storedBlobMap[key]
}
getFromMemory(key) {
return this.inMemoryBlobs.get(key)
}
getFromStorage(key) {
if (!this.storedBlobMap[key]) {
return
}
return this.storedBlob.slice.apply(this.storedBlob, this.storedBlobMap[key])
}
getDump() {
let buffers = []
let blobMap = {}
let currentBufferStart = 0
function dump(key, getBufferByKey) {
let buffer = getBufferByKey(key)
buffers.push(buffer)
blobMap[key] = [currentBufferStart, currentBufferStart + buffer.length]
currentBufferStart += buffer.length
}
for (let key of this.inMemoryBlobs.keys()) {
if (this.usedKeys.has(key)) {
dump(key, this.getFromMemory.bind(this))
}
}
for (let key of Object.keys(this.storedBlobMap)) {
if (!blobMap[key] && this.usedKeys.has(key)) {
dump(key, this.getFromStorage.bind(this))
}
}
return [buffers, blobMap]
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment