/* Copyright 2010 Aaron J. Radke Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cc.drx //java package names renamed import java.io.{File=>JFile} import java.nio.file.{Path => JPath, Paths => JPaths, Files => JFiles} import java.io.{InputStream => JIS} import java.io.{OutputStream => JOS} import java.nio.file.{WatchEvent,WatchService,WatchKey,StandardWatchEventKinds=>WEK} import scala.collection.JavaConverters._ import scala.concurrent.blocking //java packages import java.nio.file.attribute.PosixFilePermission //--Object object File{ //--alias to common stream generator formats val GZ = StreamGenerator.GZ //TODO need to make a smart convert that looks at file extensions and does the right thing the typeclass for Covnert is already there as a nice possible solution... //--file helper objects implicit object ParsableFile extends Parsable[File]{ def apply(v:String):File = File(v) } case class Loc(file:File, line:Int) final class FileStringContext(val sc:StringContext) extends AnyVal{ def file(args:Any*):File = File(sc.s(args:_*)).expandTilde } //--constructors def apply(path:JPath):File = new File(path.toFile) def apply(file:JFile):File = new File(file) def apply(path:String="."):File = new File(new JFile(path)) //--specific constructions def roots = java.io.File.listRoots.toList map apply def root = File("/") //multiple meanings for windows but the list function will list the available mounts in windows lazy val home = File(System.getProperty("user.home")) //cross platform method to generate def cwd = File(".") //curent working directory, pwd is the absolute version def pwd = File(".").canon //current working directory, pwd is the absolute canonical version /**Open a dialog box to choose a file **Blocking** */ def chooseBlocking(dialogTitle:String="Choose a file or directory", startingDirectory:File=File(".") ):Option[File] = { var block = true var choice:Option[File] = None chooseOption(dialogTitle, startingDirectory){ f => choice = f ; block = false} @tailrec def waitForSignal:Option[File] = if(block){blocking{Thread.sleep(100)}; waitForSignal} else choice waitForSignal } /**Open a dialog box to choose a file **Non-Blocking** */ def choose(dialogTitle:String="Choose a file or directory", startingDirectory:File=File(".") )(callback: File => Unit):Unit = chooseOption(dialogTitle, startingDirectory){ case Some(f) => callback(f) case _ => } private def chooseOption(dialogTitle:String="Choose a file or directory", startingDirectory:File=File(".") )(callback: Option[File] => Unit):Unit = { //Setup the chooser import javax.swing.JFileChooser import javax.swing.UIManager val previousLF = UIManager.getLookAndFeel UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName) val chooser = new JFileChooser UIManager.setLookAndFeel(previousLF) chooser setCurrentDirectory startingDirectory.file chooser setDialogTitle dialogTitle chooser setFileSelectionMode JFileChooser.FILES_AND_DIRECTORIES chooser setAcceptAllFileFilterUsed false //Setup the frame panel with an embedded chooser import javax.swing.{JFrame,JPanel} val frame = new JFrame val panel = new JPanel //Setup the chooser action commands chooser.addActionListener( new java.awt.event.ActionListener{ override def actionPerformed(e: java.awt.event.ActionEvent) = { val cmd = e.getActionCommand match { case JFileChooser.APPROVE_SELECTION => frame.setVisible(false) frame.dispose callback(Option(chooser.getSelectedFile) map File.apply) case _ => frame.setVisible(false) frame.dispose callback(None) } } } ) //frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE) frame.getContentPane.add(java.awt.BorderLayout.CENTER, panel) panel.add(chooser) val loc = { val d = Display.insets() d.loc + d.size/2 - Vec(200,200) } frame.setLocation(loc.x.toInt,loc.y.toInt) frame.pack frame.toFront frame.setAlwaysOnTop(true) frame.setVisible(true) } //--directory listings private class DirectoryListingIterator(startingDir:File) extends Iterator[File]{ private val stream = JFiles.newDirectoryStream(startingDir.file.toPath) private val it = stream.iterator private var opened = true private var autoClose = true def keepOpen:DirectoryListingIterator = {autoClose = false; this} // turn off autoClose def hasNext:Boolean = { val res = it.hasNext if(opened && autoClose && !it.hasNext){opened=false; stream.close} res } def next():File = File( it.next() ) } } //-- case class class File(val file:JFile) extends AnyVal{ def toJava:java.io.File = file //--construction //-child path def /(that:File):File = File(path + "/" + that.path) def /(filename:String):File = if(filename == "" || filename == "/") this else File(new JFile(file,filename)) def /(filename:Symbol):File = this / filename.name def /(glob:Glob):Iterator[File] = this.walk(glob, 999) def walk(glob:Glob, maxDepth:Int=0):Iterator[File] = walk(maxDepth) filter {f => glob matches f.relativeTo(this).unixPath } // def *(glob:String):Iterator[File] TODO add glob by list or by walk depth def +(postfix:String):File = File(path + postfix) //-use current file as relative absolute def apply(path:String) = { val f = File(path) if(f.isAbs) f else this / path } /**default to string should be a cross platform representation of path separators * Note: the system path form is still returned by _.path */ override def toString = unixPath// vs file.toString which is the java default //--operations def rename(that:File):Option[File] = if(file renameTo that.file) Some(that) else None def delete = file.delete //--properties def size = Bytes(file.length) def driveSize = { val f:java.io.File = root.file //get the root file object OS.Memory(free=Bytes(f.getFreeSpace),total=Bytes(f.getTotalSpace),max=Bytes(f.getUsableSpace)) } def name:String = file.getName def path:String = file.getPath def unixPath = path.replace("\\","/") def basename = { val s = name val i = s.lastIndexOf(".") if(i < 1 || i == s.size-1) s //-1 => no .; 0 => .hidden file; N-1 => dangling. else s.substring(0,i) } def ext = { val s = name val i = s.lastIndexOf(".") if(i < 1 || i == s.size-1) "" //-1 => no .; 0 => .hidden file; N-1 => dangling. else s.substring(i+1) } def mime = Mime(ext) def isDir:Boolean = file.isDirectory def isFile:Boolean = file.isFile def isAbs:Boolean = file.isAbsolute def isHidden:Boolean = file.isHidden def canRead:Boolean = file.canRead /**provide a monadic expression for the existence of a file*/ def fileOption:Option[File] = if(isFile) Some(this) else None //TODO add | syntax for orElse /**switch to a file or directory that exists else keep the last filename*/ def orElse(that: => File):File = if(isFile || isDir) this else that /**if a file exists use it as an input else use the input. This is nice to roll over to resources: File("lost.txt") or IO.resource("found.txt")*/ def orElse(that: => Input):Input = if(isFile) this.in else that /**if the file doesn't exist download it*/ def orElse(url:URL)(implicit ec:ExecutionContext):Future[File] = if(isFile) Future.successful(this) else url.async{_ copyTo this} def modified:Date = Date(file.lastModified) /**a setter way to set the last modified time, i.e a copy should sometimes do this*/ def modified_=(date:Date) = file.setLastModified(date.ms) def url:URL = new URL(canon.toURI.toURL) def abs:File = File(file.getAbsoluteFile) def expandTilde:File = if(path(0) == '~') File.home/path.drop(1) else this def canon:File = File(expandTilde.getCanonicalFile) //FIXME test this because the getAbsoluteFile was automatic before, but now f.abs.parent is required and it uses the unix path expasion instead of File parent finding //def parent:Option[File] = Option(file.getAbsoluteFile.getParentFile) map File.apply def parent:Option[File] = { val p = unixPath val n = name if(p.endsWith("/"+n)) Some(File(p.dropRight(n.size+1))) else None } def companion(altExt:String):File = { val n = this.ext.size File( (if(n == 0) path + "." else path dropRight n) + altExt ) } /**clean the file name to be a safe (without spaces and problematic characters)*/ def safe:File = { //TODO what about parent directories in the path that may not be safe val safeName = name .replace(" ","_") //messes with quotes .replace("/","_") //bad for unix .replace("\\","_") //bad for windows .replace(":","") //bad for macos parent.map{_ / safeName} getOrElse File(safeName) } def relativeTo(that:File):File = { val path = JPaths.get(that.file.toURI) relativize JPaths.get(this.file.toURI) File(path.toFile) } final def ==(that:File):Boolean = this.file.getCanonicalFile == that.file.getCanonicalFile //final def ==(that:File) = this.canon.path == that.canon.path //final def ==(that:File) = this.file == that.file /**ensures current file parent directories exist by checking or making them, returns Success[File] if successful * This can be used as a sort of monadic wrapper around succesful parents * In most cases this removes the need for an explicit mkdir, assuming you should only need a directory if you have a file * Note: this assumes a file inside the directory is specified. It only makes parent files, not the file itself * Note: we need the canonical in system version of the file to make sure the parent directory is really possible on the system */ def mkParents:Try[File] = canon.parent match { case None => Fail((s"$this does not define a feasible parent on this system")) case Some(p) => { if(p.isFile) Fail((s"parent of $this alreay exists and is not a directory")) else if(p.isDir) Success(this) //the parent already exists and is a file else Try{p.file.mkdirs} match { //use java.io.File mkdirs to make the parents case Success(true) => Success(this) case Success(false) => Fail(s"could not create parent dir $p") case Failure(ex) => Failure(ex) } } } /**ensures a directory can be made successfully */ def mkDir:Option[File] = {Try{file.mkdirs}; if(isDir) Some(this) else None } //TODO make this fail utility function a broader drx utility function, maybe at the package level private def Fail[A](msg:String):Failure[A] = Failure[A](new java.lang.Throwable(msg)) //local copy helper functions private def cp(that:File):Unit = this.in copyTo that.out private def mv(that:File):Unit = this.in copyTo that.out private def cpTry(that:File):Try[File] = Try{cp(that); that.modified = this.modified; that} /**smart copy, make sure the destination parent directories exist and preserve the src file modification time*/ def copyTo(that:File):Try[File] = that.mkParents flatMap cpTry def moveTo(that:File):Try[File] = that.mkParents flatMap { parent => val mvOpt = java.nio.file.StandardCopyOption.REPLACE_EXISTING Try{ File(java.nio.file.Files.move(this.file.toPath, that.file.toPath, mvOpt).toFile) } } def chmod(perm:Int):Unit = { def rwx(perm:Int):(Boolean,Boolean,Boolean) = ((perm & 4) > 0, (perm & 2) > 0, (perm & 1) > 0) val (ro,wo,xo) = rwx(perm / 100) val (rg,wg,xg) = rwx(perm % 100 / 10) val (ra,wa,xa) = rwx(perm % 100 % 10) //> java 7 Try{ val perms = Seq( ro -> PosixFilePermission.OWNER_READ, wo -> PosixFilePermission.OWNER_WRITE, xo -> PosixFilePermission.OWNER_EXECUTE, rg -> PosixFilePermission.GROUP_READ, wg -> PosixFilePermission.GROUP_WRITE, xg -> PosixFilePermission.GROUP_EXECUTE, ra -> PosixFilePermission.OTHERS_READ, wa -> PosixFilePermission.OTHERS_WRITE, xa -> PosixFilePermission.OTHERS_EXECUTE ).filter(_._1).map(_._2).toSet JFiles.setPosixFilePermissions(file.toPath, Java(perms) ) } getOrElse { //< java 7 val r = ro | rg | ra val w = wo | wg | wa val x = xo | xg | xa val rGlobal = rg | ra val wGlobal = wg | wa val xGlobal = xg | xa file.setReadable( r, !rGlobal) // set if any are writable, set owner only file.setWritable( w, !wGlobal) file.setExecutable(x, !xGlobal) } {} } /**Smart listing files or directories or cross platform root capabilities*/ def list:Iterator[File] = { if(path == "/" || path == "\\"){ //root path case works even for Windows by listing the available drives val roots = File.roots if(roots.size == 1) roots(0).list //if only one mount list its directories (this is the unix case and if only a C: drive) else roots.iterator } else if(this.isDir) Try{new File.DirectoryListingIterator(this)} getOrElse {println(s"warning could not list file $this"); Iterator()} //list the files in the directory if no exception else if(this.isFile) Iterator(this) //if this is a file return an iterator over a single file else if(path startsWith "~") (OS.home / path.drop(1)).list else Iterator[File]() } def root:File = { val p = this.canon.path File.roots.find(p startsWith _.path) getOrElse File.root.canon } /**walk is equivalent to walk(maxDepth=0,=>false,0) is equivalent to list */ def walk:Iterator[File] = list.flatMap{f => if(f.isDir) Iterator(f) ++ f.walk else Iterator(f) } /**walk(maxDepth=0) is equivalent to list */ def walk(maxDepth:Int):Iterator[File] = walkAtDepth(maxDepth=Some(maxDepth), skipDir = (_) => false) //don't skip any file (directory) def walkSkipIf(skipDir:File=>Boolean):Iterator[File] = walkAtDepth(maxDepth=None, skipDir=skipDir) private def walkAtDepth( maxDepth:Option[Int], skipDir:File=>Boolean = (_) => false, //don't skip directories by default currentDepth:Int = 0 //accumulator should be set ):Iterator[File] = list.flatMap{f => val notAtMaxDepth = maxDepth map {currentDepth < _} getOrElse true if(notAtMaxDepth && f.isDir && f.canRead && !skipDir(f)) Iterator(f) ++ f.walkAtDepth(maxDepth,skipDir,currentDepth+1) else Iterator(f) } //--stream generators /* private var specifiedStreamGenerator:Option[IO.StreamGenerator] = None private def findStreamGenerator:IO.StreamGenerator = { if(specifiedStreamGenerator.isDefined) specifiedStreamGenerator.get else streamGeneratorLibrary.find(_.exts contains ext) getOrElse IO.Normal } def as(sg:IO.StreamGenerator):File = {specifiedStreamGenerator = Some(sg); this} */ def in:Input = Input(new java.io.BufferedInputStream(new java.io.FileInputStream(file),8*1024)) private def mkOutput(append:Boolean):Try[Output] = mkParents.map{f => Output(new java.io.BufferedOutputStream(new java.io.FileOutputStream(f.file,append),8*1024)) } def out:Output = mkOutput(append=false).get def outAppend:Output = mkOutput(append=true).get //--file watch functions def watch(func:File => Unit)(implicit ec: ExecutionContext):WatchService = if(isDir) watch("CMD",recursive=true)(func) else watch("CM")(func) /** debounce checks fmod and create differences, delay:adds a slight delay before f-execution*/ def watch(setup:String,recursive:Boolean=false,debounce:Time=1.s,delay:Time=20.ms)(func:File => Unit)(implicit ec:ExecutionContext):WatchService = { val wek = Map( 'C' -> WEK.ENTRY_CREATE, 'M' -> WEK.ENTRY_MODIFY, 'D' -> WEK.ENTRY_DELETE ) val weks:Array[java.nio.file.WatchEvent.Kind[_]] = setup.toUpperCase.toArray map wek val modifiers = JailBreak.field[WatchEvent.Modifier]("com.sun.nio.file.SensitivityWatchEventModifier.HIGH").toOption.toList //only add mod if successfully found val dir:File = if(isDir) this else parent getOrElse this val dirPath:JPath = dir.file.toPath val watcher = dirPath.getFileSystem.newWatchService val registeredPaths = TrieMap.empty[WatchKey,JPath] //crazy map is required to back out the absolute file path from multiple registered paths def register(dir:File):Unit = { //println(s"Register watching $dir") val path = dir.file.toPath val wk = path.register(watcher, weks, modifiers:_*) registeredPaths(wk) = path } //--register register(dir) //parent if(recursive && isDir) for(f <- walk; if f.isDir) register(f) //recursively //--map for file modifications val lastModified = TrieMap.empty[File,Date] @tailrec def watch:Unit = { val key = watcher.take //Thread.sleep(1000)//1sec Java.toScala(key.pollEvents).foreach{e => /* e.kind match { case WEK.ENTRY_MODIFY => println(s"Modified $file") case WEK.ENTRY_CREATE => println(s"Created $file") case WEK.ENTRY_DELETE => println(s"Deleted $file") } */ val eventPath = e.context.asInstanceOf[JPath] val dir = File(registeredPaths.getOrElse(key,dirPath).toFile) val eventFile = dir / eventPath.toFile.getName if(isDir || (isFile && this == eventFile)){ val bounced = (lastModified contains eventFile) && (eventFile.modified - lastModified(eventFile) < debounce) if(!bounced){ delay.sleepWithoutWarning(); func(eventFile) } //Note the sleep is awkwardly required for Notepad++ causes some hickups for auto watch files that are near instantly read... } lastModified(eventFile) = eventFile.modified } if(key.reset) watch else { key.cancel; watcher.close} } Future{watch} onComplete { case Success(_) => println(s"Done watching $this") case Failure(e:java.io.FileNotFoundException) => watch //keep watching case Failure(e) => println(s"Stopped watching $this: $e") } watcher } //--conversion utilities def convert[A<:FileKind, B<:FileKind](tgt:File)(implicit fc:FileConverter[A,B], ec:EC):Future[File] = fc.convert(this, tgt) def convert[A<:FileKind, B<:FileKind](implicit fc:FileConverter[A,B], ec:EC):Future[File] = fc.convert(this) //auto detect file types def convertTo(tgt:File)(implicit fcs:FileConverters, ec:EC):Future[File] = fcs.convert(this,tgt) }