How do I save multiple files in a package folder from a SwiftUI Document Based App? - file

I'm developing a SwiftUI document-based app that contains some easily serializable data plus multiple images. I'd like to save the document as a package (i.e, a folder) with one file containing the easily serialized data and a subfolder containing the images as separate files. My package directory should look something like this:
<UserChosenName.pspkg>/. // directory package containing my document data and images
PhraseSet.dat // regular file with serialized data from snapshot
Images/ // subdirectory for images (populated directly from my app as needed)
Image0.png
Image1.png
....
I've created a FileWrapper subclass that sets up the directory structure and adds the serialized snapshot appropriately but when I run the app in an iOS simulator and click on "+" to create a new document the app runs through the PkgFileWrapper init() and write() without error but returns to the browser window without apparently creating anything. I have declared that the Exported and Imported Type Identifiers conform to "com.apple.package". Can anyone suggest a way to get this working?
The PkgFileWrapper class looks like this:
class PkgFileWrapper: FileWrapper {
var snapshot: Data
init(withSnapshot: Data) {
self.snapshot = withSnapshot
let sWrapper = FileWrapper(regularFileWithContents: snapshot)
let dWrapper = FileWrapper(directoryWithFileWrappers: [:])
super.init(directoryWithFileWrappers: ["PhraseSet.dat" : sWrapper,
"Images" : dWrapper ])
// NOTE: Writing of images is done outside
// of the ReferenceFileDocument functionality.
}
override func write(to: URL,
options: FileWrapper.WritingOptions,
originalContentsURL: URL?) throws {
try super.write(to: to, options: options,
originalContentsURL: originalContentsURL)
}
required init?(coder inCoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

The solution is to not override PkgFileWrapper.write(...). If the directory structure is set up correctly in the init(...) then the files and directories will be created automatically. The overridden write(...) function above has now been corrected.
If you want to write an image to the Images subdirectory, you could do something like the following:
func addImage(image: UIImage, name: String) {
let imageData = image.pngData()!
imageDirWrapper.addRegularFile(withContents: imageData,
preferredFilename: name)
}
The value of imageDirWrapper is the directory wrapper corresponding to the directory that holds your images, as created in PkgFileWrapper.init() above. A key concept you need to keep in mind here is that the "write" function will get called automatically at the appropriate time - you don't explicitly write out your image data. The ReferenceFileDocument class will arrange for that and will also arrange for your app to be passed the appropriate URL for setting up your file wrappers.
The imageDirWrapper variable is set in the required init(...) for the ReferenceFileDocument protocol:
required init(configuration: ReadConfiguration) throws {
phraseSet = PhraseSet()
if configuration.file.isDirectory {
if let subdir = configuration.file.fileWrappers {
// first load in the phraseSet
for (name, wrapper) in subdir {
if name == PkgFileWrapper.phraseSetFileName {
if let data = wrapper.regularFileContents {
phraseSet = try PhraseSet(json: data)
}
}
}
// next load in the images and put them into the phrases.
for (name, wrapper) in subdir {
if name == PkgFileWrapper.imageDirectoryName {
if let imageDir = wrapper.fileWrappers {
imageDirWrapper = wrapper
for (iName, iWrapper) in imageDir {
print("image file: \(iName)")
if let d = iWrapper.regularFileContents {
for p in phraseSet.phrases {
if p.imageName == iName {
// TBD: downsample
var uiD = ImageData(data: d)
if doDownSample {
uiD.uiimageData = downsample(data: d,
to: imageSize)
} else {
_ = uiD.getUIImage()
}
images[iName] = uiD
}
}
}
}
}
}
}
}
} else {
throw CocoaError(.fileReadCorruptFile)
}
You can see here how imageDirWrapper is set by looking through the passed-in directory's subdirectories for the image directory name. Also some bonus code: it first looks through the passed-in directory for the data file and loads it in; then it looks for the image directory and processes it.

Related

How to edit text files with google apps script? It must be done with Drive Advanced Api [duplicate]

I want Google script variable data into Google drive as text file and update that text file regulatory via Google script!
The following code creates a text file and writes the data on it.I wonder how i can update the text file later ?
function createTextFile()
{
name="testFile.txt";
name2="testFolder";
var content = "this is text data to be written in text file";
var dir = DriveApp.getFoldersByName(name2).next()
var file = dir.createFile(name, content);
}
Just in case anyone's still interested 3 years later... :)
The following will create or append, assuming content is text:
function createOrAppendFile() {
var fileName="myfile";
var folderName="myfolder";
var content = "this is text data to be written in text file";
// get list of folders with matching name
var folderList = DriveApp.getFoldersByName(folderName);
if (folderList.hasNext()) {
// found matching folder
var folder = folderList.next();
// search for files with matching name
var fileList = folder.getFilesByName(fileName);
if (fileList.hasNext()) {
// found matching file - append text
var file = fileList.next();
var combinedContent = file.getBlob().getDataAsString() + content;
file.setContent(combinedContent);
}
else {
// file not found - create new
folder.createFile(fileName, content);
}
}
}
In summary, as there appears to be no append function, you instead simply read the existing file contents, add the new content, then (over)write the combined content back to the file.
*tip - add a "\n" between the old and new content for a new line separator
update Override the contents if the file is exists.
function saveData(folder, fileName, contents) {
var filename = "testFile.txt";
var children = folder.getFilesByName(filename);
var file = null;
if (children.hasNext()) {
file = children.next();
file.setContent(contents);
} else {
file = folder.createFile(filename, contents);
}
}
function test() {
var folders = DriveApp.getFoldersByName("testFolder");
if (folders.hasNext()) {
var folder = folders.next();
saveData(folder, "testfile.txt", "HelloWorld");
saveData(folder, "testfile.txt", "Welcome");
}
}

Reading & writing data to a .txt file using vscode extension api

So I am new to this vscode extension api. I have this functionality where I need to take input from the user when they click on certain line and then get the 1). input value, line number and file name and 2). store it to a text file.
I am done with the first part, I am getting the data everything. Now I have to just write it to the file and if there is data already, new data should be appended not overwritten.
I have tried using fs.writeFileSync(filePath, data) and readFileSync but nothing, I do not know if I am doing it correctly. If someone can point me in the right direction I am just blank at this stage?
Any help would be appreciated, Thanks in advance.
The FS node module works fine in an extension. I use it all the time for file work in my extension. Here's a helper function to export something to a file with error handling:
/**
* Asks the user for a file to store the given data in. Checks if the file already exists and ask for permission to
* overwrite it, if so. Also copies a number extra files to the target folder.
*
* #param fileName A default file name the user can change, if wanted.
* #param filter The file type filter as used in showSaveDialog.
* #param data The data to write.
* #param extraFiles Files to copy to the target folder (e.g. css).
*/
public static exportDataWithConfirmation(fileName: string, filters: { [name: string]: string[] }, data: string,
extraFiles: string[]): void {
void window.showSaveDialog({
defaultUri: Uri.file(fileName),
filters,
}).then((uri: Uri | undefined) => {
if (uri) {
const value = uri.fsPath;
fs.writeFile(value, data, (error) => {
if (error) {
void window.showErrorMessage("Could not write to file: " + value + ": " + error.message);
} else {
this.copyFilesIfNewer(extraFiles, path.dirname(value));
void window.showInformationMessage("Diagram successfully written to file '" + value + "'.");
}
});
}
});
}
And here an example where I read a file without user intervention:
this.configurationDone.wait(1000).then(() => {
...
try {
const testInput = fs.readFileSync(args.input, { encoding: "utf8" });
...
} catch (e) {
...
}
...
});

how to make an array of urls from a folder (swift, audio)

I´m looking for a way to make an array of urls from the contents of a folder containing soundfiles so that i can call them up with AVAudioplayer later.
I use a stepper and 3 variables,
the first one is called nmbTracks to define the number of playable soundfiles, and holds the steppers maxvalue
the second is called currentActiveTrack and defines which file the player should be playing.
the 3rd one is called audioURL and is used to feed the player aswell as retrieving the name of of the file which gets outputed to a label.
the user can then step through the soundfiles with the stepper.
so far I got everything working with some files that i have but I can´t figure out how to make an array of urls nor how to get urls from the contents of a folder, next step is to let the user import their own files into that folder.
any help would be greatly appreciated, I´m sure there is an easier way to do this aswell
I have a couple of options. The first one returns an array of URLs and if there is an error fetching the folder returns an empty array.
func getFolderContentsURLs(_ folderPath: String) -> [URL] {
let fm = FileManager()
guard let contents = try? fm.contentsOfDirectory(atPath: folderPath) else { return [] }
return contents.compactMap { URL.init(fileURLWithPath: $0) }
}
You can display the result like this:
let songsURLs = getFolderContentsURLs("/")
songsURLs.forEach {
print($0.absoluteString)
}
The second option return an optional array of URLs, on error returns nil
func getFolderContentsURLsOrNil(_ folderPath: String) -> [URL]? {
let fm = FileManager()
guard let contents = try? fm.contentsOfDirectory(atPath: folderPath) else { return nil }
return contents.compactMap { URL.init(fileURLWithPath: $0) }
}
To go through the results you can do it like this:
if let songsURLs2 = getFolderContentsURLsOrNil("NotExistingPath") {
songsURLs2.forEach {
print($0.absoluteString)
}
} else {
print("Unable to fetch contents")
}
Responding to #user12184258 comment about returning only audio files you can apply a filter after getting the folder contents. The filter can check each file using UTTypeConformsTo from MobileCoreServices
let contentsFolder = getFolderContentsURLs("/path/to/folder")
let audioFiles = contentsFolder
.filter { path in
// Try to create uniform type identifier using file's extension
guard let uti =
UTTypeCreatePreferredIdentifierForTag(
kUTTagClassFilenameExtension,
path.pathExtension as CFString, nil) else { return false }
// Checks conformance of UTI with kUTTypeAudio (abstract type for audio)
return UTTypeConformsTo((uti.takeRetainedValue()), kUTTypeAudio)
}
I found this method here. Also Apple has a very useful System-Declared Uniform Type Identifiers you can check, if you want only mp3 files you can use kUTTypeMP3.

Laravel insert millions of database rows from models

I have a text file that contains comma delineated values representing data set with each row within the string. There are about 2 million of them and I want to parse the string, create Laravel models from them and store each as a row in my database.
At this time, I have a class that parses the file line by line and creates a model for each as follows:
class LargeFileParser{
// File Reference
protected $file;
// Check if file exists and create File Object
public function __construct($filename, $mode="r"){
if(!file_exists($filename)){
throw new Exception("File not found");
}
$this->file = new \SplFileObject($filename, $mode);
}
// Iterate through the text or binary document
public function iterate($type = "Text", $bytes = NULL)
{
if ($type == "Text") {
return new \NoRewindIterator($this->iterateText());
} else {
return new \NoRewindIterator($this->iterateBinary($bytes));
}
}
// Handle Text iterations
protected function iterateText()
{
$count = 0;
while (!$this->file->eof()) {
yield $this->file->fgets();
$count++;
}
return $count;
}
// Handle binary iterations
protected function iterateBinary($bytes)
{
$count = 0;
while (!$this->file->eof()) {
yield $this->file->fread($bytes);
$count++;
}
}
}
I then have a controller (I want to be able to run this migration via a route occasionally) that handles creating and inserting the models into the database:
class CarrierDataController extends Controller
{
// Store the data keys for a carrier model
protected $keys;
//Update the Carrier database with the census info
public function updateData(){
// File reference
$file = new LargeFileParser('../storage/app/CENSUS.txt');
//Get iterator for the file
$iterator = $file->iterate("Text");
// For each iterator, store the data object as a carrier in the database
foreach ($iterator as $index => $line) {
// First line sets the keys specified in the file
if($index == 0){
$this->keys = str_getcsv(strtolower($line), ",", '"');
}
// The rest hold the data for each model
else{
if ($index <= 100) {
// Parse the data to an array
$dataArray = str_getcsv($line, ",", '"');
// Get a data model
$dataModel = $this->createCarrierModel(array_combine($this->keys, $dataArray));
// Store the data
$this->storeData($dataModel);
}
else{
break;
}
}
}
}
// Return a model for the data
protected function createCarrierModel($dataArray){
$carrier = Carrier::firstOrNew($dataArray);
return $carrier;
}
// Store the carrier data in the database
protected function storeData($data){
$data->save();
}
}
This works perfectly...that is while I'm limiting the function to 100 inserts. If I remove this check and allow it to run this function over the entire 2 million data sets, it no longer works. Either there is a timeout, or if I remove the timeout via something like ini_set('max_execution_time', 6000); I eventually get a "failed to respond" message from the browser.
My assumption is that there needs to be some sort of chunking in place, but I'm honestly not sure of the best approach for handling this volume.
Thank you in advance for any suggestions you may have.
I would create an artisan command who handles the import rather than doing this via the browser. Do you like to let the user wait until this big file is imported? What happens if he moves uses the back button or closes the page?
If you want or need to have some kind of user interaction, like the user uploads the file and clicks on an Import button, push the import to a job queue using e.g. Beanstalk. The aforementioned artisan will be run and import the stuff and if its done, you can send the user an e-mail or a slack notification. If you need some UI interaction you can make the request via ajax and that script makes request to an API endpoint requesting the status of the import or since its asynchron, waiting for completion and shows some UI notification, stops a spinner or in error case, shows an error message.

Archiving and unarchiving Arrays in the DocumentDirectory in Swift

I am trying to save a simple array of objects in the persistent memory by executing the following code:
let fileManager=NSFileManager()
let urls = fileManager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
if urls.count>0{
let localDocumentsDirectory=urls[0]
let archivePath=localDocumentsDirectory.URLByAppendingPathExtension("meditations.archive")
NSKeyedArchiver.archiveRootObject(self.meditationsArray, toFile: archivePath.path!)
let restored=NSKeyedUnarchiver.unarchiveObjectWithFile(archivePath.path!)
print("restored \(restored)")
}
}
Yet, when I print the restored date as in the code I find nil.
Conversely, if I use the CachesDirectory the array is soon after restored fine,
but when I reopen the app and try to load the data, it is lost. What is correct way to persistently save data?
I think the problem is that your are using URLByAppendingPathExtension, when you should be using URLByAppendingPathComponent. The "path extension" is the file extension, so your archivePath is "~/Documents.meditations.archive". It might be temporarily working with the CachesDirectory, because it's putting the data into a temporary file somewhere, or maybe just reading it back from memory. This should fix it:
let fileManager = NSFileManager()
let documentDirectoryUrls = fileManager.URLsForDirectory(.DocumentDirectory, .UserDomainMask)
if let documentDirectoryUrl = documentDirectoryUrls.first {
let fileUrl = documentDirectoryUrl.URLByAppendingPathComponent("meditations.archive")
// Also, take advantage of archiveRootObject's return value to check if
// the file was saved successfully, and safely unwrap the `path` property
// of the URL. That will help you catch any errors.
if let path = fileUrl.path {
let success = NSKeyedArchiver.archiveRootObject(meditationArray, toFile: path)
if !success {
print("Unable to save array to \(path)")
}
} else {
print("Invalid path")
}
} else {
print("Unable to find DocumentDirectory for the specified domain mask.")
}
I faced the same issue, I was unable to archive and unarchive array of objects using NSKeyedArchiver, I think the issue is that I'm using the below method :
NSKeyedArchiver.archiveRootObject(arrayOfItems, toFile: FileManager.getFileURL("My-File-Name")!)
I think this method is for archiving Objects, not array of Objects.
Anyway, I found a solution to my problem, by wrapping the whole array in an object, check below :
let myArrayItemsContainer = ArrayItemsContainer()
myArrayItemsContainer.allItems = arrayOfItems
NSKeyedArchiver.archiveRootObject(myArrayItemsContainer, toFile: FileManager.getFileURL("My-File-Name")!)
and I used the below code to unarchive my object :
NSKeyedUnarchiver.unarchiveObject(withFile: FileManager.getFileURL("My-File-Name")!) as? ArrayItemsContainer
Also I used this extension for using FileManager.getFileURL
public extension FileManager {
/// Returns the URL of the file given a name
///
/// - Parameter fileName: The file name of the file + extension
/// - Returns: The URL as String
static func getFileURL(_ fileName: String) -> String? {
let fileURL = FileManager().urls(for: FileManager.SearchPathDirectory.documentDirectory, in: FileManager.SearchPathDomainMask.userDomainMask).first
return (fileURL?.appendingPathComponent(fileName).path)
}
}

Resources