This commit is contained in:
Ty Clifford
2026-07-04 20:14:52 -04:00
parent 94aea8501f
commit bd0bfdc09f
4 changed files with 751 additions and 0 deletions
+19
View File
@@ -42,6 +42,25 @@ php cli/blog.php comments:spam 2
php cli/blog.php comments:delete 3
```
## Import Bludit
Import from a local directory on the server. The source can be the old Bludit root or the old `bl-content` folder.
```bash
php cli/import-bludit.php /var/www/old-bludit --dry-run
php cli/import-bludit.php /var/www/old-bludit
php cli/import-bludit.php /var/www/old-bludit/bl-content
php cli/import-bludit.php --source=/var/www/old-bludit --overwrite
```
The importer preserves original publish dates, modified dates, post/static-page type, draft/published state, categories, tags, authors, cover image metadata, allow-comments flags, page order, page-local files, and `bl-content/uploads`.
It also scans local comment export files under the old `bl-content/databases` and `bl-content/workspaces` directories. To point it at a specific local comment export:
```bash
php cli/import-bludit.php /var/www/old-bludit --comments=/var/www/old-bludit/comments.json
```
## Stats
Visits are appended to `storage/stats/visits.csv` with a header row suitable for spreadsheet import.
+42
View File
@@ -87,6 +87,42 @@ final class CommentService
];
}
/** @param array<string, mixed> $data */
public function import(string $slug, array $data): int
{
$createdAt = $this->normalizeDate((string) ($data['created_at'] ?? $data['date'] ?? ''));
$updatedAt = $this->normalizeDate((string) ($data['updated_at'] ?? $data['modified'] ?? $createdAt));
$status = (string) ($data['status'] ?? 'approved');
if (!in_array($status, ['pending', 'approved', 'spam'], true)) {
$status = match ($status) {
'published', 'published-comment', 'visible', '1', 'true' => 'approved',
'trash', 'deleted', 'spam-comment' => 'spam',
default => 'pending',
};
}
$statement = $this->pdo->prepare(
'INSERT INTO comments
(slug, author, email, website, body, status, ip_hash, user_agent, created_at, updated_at)
VALUES
(:slug, :author, :email, :website, :body, :status, :ip_hash, :user_agent, :created_at, :updated_at)'
);
$statement->execute([
'slug' => $slug,
'author' => trim((string) ($data['author'] ?? $data['name'] ?? 'Imported')),
'email' => trim((string) ($data['email'] ?? '')),
'website' => trim((string) ($data['website'] ?? $data['site_url'] ?? $data['url'] ?? '')),
'body' => trim((string) ($data['body'] ?? $data['content'] ?? $data['comment'] ?? '')),
'status' => $status,
'ip_hash' => trim((string) ($data['ip_hash'] ?? '')),
'user_agent' => substr((string) ($data['user_agent'] ?? $data['agent'] ?? 'import'), 0, 300),
'created_at' => $createdAt,
'updated_at' => $updatedAt,
]);
return (int) $this->pdo->lastInsertId();
}
/** @return array<int, array<string, mixed>> */
public function list(?string $status = null, ?string $slug = null): array
{
@@ -144,4 +180,10 @@ final class CommentService
return hash_hmac('sha256', $ip, $salt);
}
private function normalizeDate(string $value): string
{
$timestamp = strtotime($value);
return date('c', $timestamp ?: time());
}
}
+44
View File
@@ -254,6 +254,50 @@ final class ContentRepository
return $this->find($slug, true) ?? [];
}
/** @param array<string, mixed> $metadata */
public function saveImported(string $slug, array $metadata, string $markdown, bool $rebuildIndex = true): array
{
$slug = self::slugify($slug !== '' ? $slug : (string) ($metadata['title'] ?? 'untitled'));
if ($slug === '') {
throw new RuntimeException('A title or slug is required.');
}
$directory = $this->pagesRoot . '/' . $slug;
if (!is_dir($directory)) {
mkdir($directory, 0775, true);
}
$date = (string) ($metadata['date'] ?? date('c'));
$metadata = array_replace([
'title' => self::titleFromSlug($slug),
'type' => 'post',
'status' => 'published',
'date' => $date,
'modified' => $metadata['modified'] ?? $date,
'category' => 'Notes',
'tags' => [],
'summary' => '',
'author' => 'Editor',
'cover' => '',
'allow_comments' => true,
'menu_order' => 0,
], $metadata);
$metadata['slug'] = $slug;
$metadata['tags'] = self::normalizeList($metadata['tags'] ?? []);
$metadata['allow_comments'] = self::toBool($metadata['allow_comments'] ?? true);
file_put_contents($directory . '/index.txt', $this->serialize($metadata, $markdown));
$timestamp = strtotime((string) ($metadata['modified'] ?? $metadata['date'])) ?: time();
touch($directory . '/index.txt', $timestamp);
if ($rebuildIndex) {
$this->rebuildIndex();
}
return $this->find($slug, true) ?? [];
}
public function delete(string $slug): bool
{
$slug = self::slugify($slug);
+646
View File
@@ -0,0 +1,646 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
use NeonBlog\ContentRepository;
$app = require dirname(__DIR__) . '/app/bootstrap.php';
$repo = $app->repository();
$comments = $app->comments();
[$args, $opts] = parseArguments(array_slice($_SERVER['argv'] ?? [], 1));
if (isset($opts['help']) || isset($opts['h']) || ($args === [] && !isset($opts['source']))) {
help();
exit(isset($opts['help']) || isset($opts['h']) ? 0 : 1);
}
$source = (string) ($opts['source'] ?? $args[0]);
$dryRun = isset($opts['dry-run']);
$overwrite = isset($opts['overwrite']);
$skipUploads = isset($opts['skip-uploads']);
$skipComments = isset($opts['skip-comments']);
try {
$paths = resolveBluditSource($source);
$siteDb = readDatabase($paths['databases'] . '/site.php')
?: readDatabase($paths['databases'] . '/site.json');
$timezone = (string) ($opts['source-timezone'] ?? $siteDb['timezone'] ?? date_default_timezone_get());
$pagesDb = readDatabase($paths['databases'] . '/pages.php')
?: readDatabase($paths['databases'] . '/pages.json');
$categoriesDb = readDatabase($paths['databases'] . '/categories.php')
?: readDatabase($paths['databases'] . '/categories.json');
$tagsDb = readDatabase($paths['databases'] . '/tags.php')
?: readDatabase($paths['databases'] . '/tags.json');
$entries = discoverPages($paths['pages'], $pagesDb);
if ($entries === []) {
fail('No Bludit pages were found in ' . $paths['pages']);
}
$summary = [
'source' => $paths['content'],
'dry_run' => $dryRun,
'pages_found' => count($entries),
'content_imported' => 0,
'uploads_copied' => 0,
'page_assets_copied' => 0,
'comments_imported' => 0,
'skipped' => [],
];
$slugMap = [];
$reservedSlugs = [];
foreach ($entries as $entry) {
$oldKey = (string) $entry['key'];
$row = is_array($entry['db']) ? $entry['db'] : [];
$contentPath = $entry['path'];
$markdown = is_file($contentPath) ? (string) file_get_contents($contentPath) : '';
$baseSlug = slugFromBluditKey($oldKey);
$slug = uniqueSlug($repo, $baseSlug, $reservedSlugs, $overwrite);
$reservedSlugs[$slug] = true;
$metadata = metadataFromBludit($oldKey, $row, $markdown, $categoriesDb, $tagsDb, $timezone);
$slugMap[$oldKey] = $slug;
if (!$dryRun) {
$repo->saveImported($slug, $metadata, $markdown, false);
}
$summary['page_assets_copied'] += copyPageAssets(dirname($contentPath), BLOG_ROOT . '/bl-content/pages/' . $slug, $dryRun);
$summary['content_imported']++;
printf(
"%s %-10s %-10s %s -> %s\n",
$dryRun ? 'Would import' : 'Imported',
$metadata['type'],
$metadata['status'],
$oldKey,
$slug
);
}
if (!$skipUploads && is_dir($paths['uploads'])) {
$summary['uploads_copied'] = copyTree($paths['uploads'], BLOG_ROOT . '/bl-content/uploads', $dryRun);
line(($dryRun ? 'Would copy' : 'Copied') . ' uploads from ' . $paths['uploads']);
}
if (!$skipComments) {
$commentFiles = [];
if (isset($opts['comments'])) {
$commentFiles[] = (string) $opts['comments'];
}
$commentFiles = array_merge($commentFiles, discoverCommentFiles($paths['content']));
$seenCommentFiles = [];
foreach ($commentFiles as $commentFile) {
$real = realpath($commentFile);
if ($real === false || isset($seenCommentFiles[$real])) {
continue;
}
$seenCommentFiles[$real] = true;
$records = readComments($real);
foreach ($records as $record) {
$oldSlug = (string) ($record['slug'] ?? '');
$slug = $slugMap[$oldSlug] ?? $slugMap[str_replace('-', '/', $oldSlug)] ?? ContentRepository::slugify($oldSlug);
$body = trim((string) ($record['body'] ?? $record['content'] ?? $record['comment'] ?? ''));
if ($slug === '' || $body === '') {
continue;
}
if (!$dryRun) {
$comments->import($slug, [
'author' => $record['author'] ?? $record['name'] ?? 'Imported',
'email' => $record['email'] ?? '',
'website' => $record['website'] ?? $record['url'] ?? '',
'body' => $body,
'status' => $record['status'] ?? 'approved',
'created_at' => normalizeDate((string) ($record['date'] ?? $record['created_at'] ?? ''), $timezone),
'updated_at' => normalizeDate((string) ($record['modified'] ?? $record['updated_at'] ?? $record['date'] ?? ''), $timezone),
'user_agent' => $record['user_agent'] ?? $record['agent'] ?? 'bludit-import',
]);
}
$summary['comments_imported']++;
}
}
}
if (!$dryRun) {
$repo->rebuildIndex();
}
line('');
line('Import summary:');
foreach ($summary as $key => $value) {
if (is_array($value)) {
continue;
}
line(' ' . $key . ': ' . (is_bool($value) ? ($value ? 'yes' : 'no') : (string) $value));
}
} catch (Throwable $error) {
fail($error->getMessage());
}
function help(): void
{
echo <<<TEXT
Import Bludit content from a local directory on this server.
Usage:
php cli/import-bludit.php /var/www/old-bludit
php cli/import-bludit.php /var/www/old-bludit/bl-content
php cli/import-bludit.php --source=/var/www/old-bludit --dry-run
Options:
--source=/path Local Bludit root or bl-content directory.
--dry-run Show what would import without writing files.
--overwrite Reuse matching slugs instead of adding numeric suffixes.
--skip-uploads Do not copy bl-content/uploads.
--skip-comments Do not scan/import local comment data.
--comments=/path/file Extra local JSON/PHP/CSV comment export to import.
--source-timezone=Zone Timezone for Bludit database dates without offsets.
Preserves:
Original publish dates, modified dates, static pages, post status, categories,
tags, authors, cover image metadata, allow-comments flags, page order, uploads,
page-local files, and best-effort local comments.
TEXT;
}
/** @return array{0: array<int, string>, 1: array<string, mixed>} */
function parseArguments(array $argv): array
{
$args = [];
$opts = [];
foreach ($argv as $arg) {
if (str_starts_with($arg, '--')) {
$option = substr($arg, 2);
if (str_contains($option, '=')) {
[$key, $value] = explode('=', $option, 2);
$opts[$key] = $value;
} else {
$opts[$option] = true;
}
} elseif (str_starts_with($arg, '-')) {
$opts[ltrim($arg, '-')] = true;
} else {
$args[] = $arg;
}
}
return [$args, $opts];
}
/** @return array{root: string, content: string, pages: string, databases: string, uploads: string} */
function resolveBluditSource(string $source): array
{
$path = realpath($source);
if ($path === false || !is_dir($path)) {
fail('Source directory not found or not readable: ' . $source);
}
if (is_dir($path . '/bl-content')) {
$root = $path;
$content = $path . '/bl-content';
} elseif (basename($path) === 'bl-content' || is_dir($path . '/pages')) {
$content = $path;
$root = dirname($path);
} else {
fail('Source must be a Bludit root or a bl-content directory: ' . $source);
}
$pages = $content . '/pages';
$databases = $content . '/databases';
if (!is_dir($pages)) {
fail('Bludit pages directory not found: ' . $pages);
}
return [
'root' => $root,
'content' => $content,
'pages' => $pages,
'databases' => is_dir($databases) ? $databases : $content,
'uploads' => $content . '/uploads',
];
}
/** @return array<string, mixed> */
function readDatabase(string $path): array
{
if (!is_file($path)) {
return [];
}
$raw = (string) file_get_contents($path);
$json = trim($raw);
$firstObject = strpos($json, '{');
$firstArray = strpos($json, '[');
$start = false;
if ($firstObject !== false && $firstArray !== false) {
$start = min($firstObject, $firstArray);
} elseif ($firstObject !== false) {
$start = $firstObject;
} elseif ($firstArray !== false) {
$start = $firstArray;
}
if ($start !== false) {
$json = substr($json, $start);
}
$decoded = json_decode($json, true);
return is_array($decoded) ? $decoded : [];
}
/** @return array<int, array{key: string, path: string, db: array<string, mixed>}> */
function discoverPages(string $pagesRoot, array $pagesDb): array
{
$entries = [];
if ($pagesDb !== []) {
foreach ($pagesDb as $key => $row) {
$file = $pagesRoot . '/' . $key . '/index.txt';
if (is_file($file)) {
$entries[] = [
'key' => (string) $key,
'path' => $file,
'db' => is_array($row) ? $row : [],
];
}
}
}
if ($entries !== []) {
return $entries;
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($pagesRoot, FilesystemIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if (!$file->isFile() || $file->getFilename() !== 'index.txt') {
continue;
}
$directory = dirname($file->getPathname());
$key = trim(str_replace($pagesRoot, '', $directory), DIRECTORY_SEPARATOR);
$entries[] = [
'key' => str_replace(DIRECTORY_SEPARATOR, '/', $key),
'path' => $file->getPathname(),
'db' => [],
];
}
usort($entries, static fn (array $a, array $b): int => strcmp($a['key'], $b['key']));
return $entries;
}
/** @param array<string, mixed> $row @param array<string, mixed> $categories @param array<string, mixed> $tags */
function metadataFromBludit(string $key, array $row, string $markdown, array $categories, array $tags, string $timezone): array
{
$bluditType = (string) ($row['type'] ?? 'published');
$type = $bluditType === 'static' ? 'page' : 'post';
$status = in_array($bluditType, ['published', 'static', 'sticky'], true) ? 'published' : 'draft';
$categoryKey = (string) ($row['category'] ?? '');
$coverImage = (string) ($row['coverImage'] ?? '');
if ($coverImage !== '' && !filter_var($coverImage, FILTER_VALIDATE_URL)) {
$uuid = (string) ($row['uuid'] ?? '');
$coverImage = $uuid !== ''
? '/bl-content/uploads/pages/' . $uuid . '/' . ltrim($coverImage, '/')
: '/bl-content/uploads/' . ltrim($coverImage, '/');
}
return [
'title' => trim((string) ($row['title'] ?? '')) !== ''
? (string) $row['title']
: titleFromMarkdownOrKey($markdown, $key),
'type' => $type,
'status' => $status,
'date' => normalizeDate((string) ($row['date'] ?? ''), $timezone),
'modified' => normalizeDate((string) ($row['dateModified'] ?? $row['date'] ?? ''), $timezone),
'category' => categoryName($categoryKey, $categories),
'tags' => tagNames($row['tags'] ?? [], $tags),
'summary' => (string) ($row['description'] ?? ''),
'author' => (string) ($row['username'] ?? 'Imported'),
'cover' => $coverImage,
'allow_comments' => (bool) ($row['allowComments'] ?? true),
'menu_order' => (int) ($row['position'] ?? 0),
];
}
function normalizeDate(string $value, string $timezone): string
{
$value = trim($value);
if ($value === '') {
return date('c');
}
try {
$zone = new DateTimeZone($timezone);
} catch (Throwable) {
$zone = new DateTimeZone(date_default_timezone_get());
}
try {
$date = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $value, $zone)
?: DateTimeImmutable::createFromFormat(DateTimeInterface::ATOM, $value)
?: new DateTimeImmutable($value, $zone);
} catch (Throwable) {
$date = new DateTimeImmutable('now', $zone);
}
return $date->format(DateTimeInterface::ATOM);
}
function categoryName(string $key, array $categories): string
{
if ($key !== '' && isset($categories[$key]) && is_array($categories[$key])) {
return (string) ($categories[$key]['name'] ?? $key);
}
return $key !== '' ? ucwords(str_replace('-', ' ', $key)) : 'Notes';
}
function tagNames(mixed $rawTags, array $tagsDb): array
{
if (is_string($rawTags)) {
return ContentRepository::normalizeList($rawTags);
}
if (!is_array($rawTags)) {
return [];
}
$tags = [];
foreach ($rawTags as $key => $value) {
if (is_string($value) && $value !== '') {
$tags[] = $value;
} elseif (is_string($key) && isset($tagsDb[$key]) && is_array($tagsDb[$key])) {
$tags[] = (string) ($tagsDb[$key]['name'] ?? $key);
} elseif (is_string($key)) {
$tags[] = ucwords(str_replace('-', ' ', $key));
}
}
return ContentRepository::normalizeList($tags);
}
function titleFromMarkdownOrKey(string $markdown, string $key): string
{
foreach (explode("\n", $markdown) as $line) {
if (preg_match('/^#\s+(.+)$/', trim($line), $matches)) {
return trim($matches[1]);
}
}
return ucwords(str_replace(['-', '/'], ' ', $key));
}
function slugFromBluditKey(string $key): string
{
return ContentRepository::slugify(str_replace('/', '-', $key));
}
/** @param array<string, bool> $reserved */
function uniqueSlug(ContentRepository $repo, string $baseSlug, array $reserved, bool $overwrite): string
{
$baseSlug = $baseSlug !== '' ? $baseSlug : 'imported-page';
if ($overwrite) {
return $baseSlug;
}
$slug = $baseSlug;
$i = 2;
while (isset($reserved[$slug]) || $repo->find($slug, true) !== null) {
$slug = $baseSlug . '-' . $i;
$i++;
}
return $slug;
}
function copyPageAssets(string $sourceDir, string $targetDir, bool $dryRun): int
{
$count = 0;
foreach (glob($sourceDir . '/*') ?: [] as $path) {
if (basename($path) === 'index.txt') {
continue;
}
$count += copyTree($path, $targetDir . '/' . basename($path), $dryRun);
}
return $count;
}
function copyTree(string $source, string $target, bool $dryRun): int
{
if (!file_exists($source)) {
return 0;
}
$sourceReal = realpath($source);
$targetReal = realpath($target);
if ($sourceReal !== false && $targetReal !== false && $sourceReal === $targetReal) {
return 0;
}
if ($dryRun) {
if (is_file($source) || is_link($source)) {
return 1;
}
$count = 0;
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($source, FilesystemIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if ($file->isFile() || $file->isLink()) {
$count++;
}
}
return $count;
}
if (is_link($source)) {
$real = realpath($source);
return $real === false ? 0 : copyTree($real, $target, false);
}
if (is_file($source)) {
if (!is_dir(dirname($target))) {
mkdir(dirname($target), 0775, true);
}
copy($source, $target);
touch($target, filemtime($source) ?: time());
return 1;
}
if (!is_dir($target)) {
mkdir($target, 0775, true);
}
$count = 0;
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($source, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $item) {
$relative = substr($item->getPathname(), strlen($source) + 1);
$destination = $target . '/' . $relative;
if ($item->isDir() && !$item->isLink()) {
if (!is_dir($destination)) {
mkdir($destination, 0775, true);
}
} else {
$count += copyTree($item->getPathname(), $destination, false);
}
}
return $count;
}
/** @return array<int, string> */
function discoverCommentFiles(string $contentRoot): array
{
$files = [];
foreach ([$contentRoot . '/databases', $contentRoot . '/workspaces'] as $root) {
if (!is_dir($root)) {
continue;
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if (!$file->isFile()) {
continue;
}
$name = strtolower($file->getFilename());
$extension = strtolower(pathinfo($name, PATHINFO_EXTENSION));
if (str_contains($name, 'comment') && in_array($extension, ['php', 'json', 'csv', 'sqlite', 'db'], true)) {
$files[] = $file->getPathname();
}
}
}
return $files;
}
/** @return array<int, array<string, mixed>> */
function readComments(string $path): array
{
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
if ($extension === 'csv') {
return readCommentCsv($path);
}
if (in_array($extension, ['sqlite', 'db'], true)) {
return readCommentSqlite($path);
}
return collectComments(readDatabase($path));
}
/** @return array<int, array<string, mixed>> */
function readCommentCsv(string $path): array
{
$handle = fopen($path, 'rb');
if (!$handle) {
return [];
}
$header = fgetcsv($handle, null, ',', '"', '') ?: [];
$records = [];
while (($row = fgetcsv($handle, null, ',', '"', '')) !== false) {
$record = array_combine($header, $row);
if (is_array($record)) {
$records[] = normalizeCommentRecord($record);
}
}
fclose($handle);
return $records;
}
/** @return array<int, array<string, mixed>> */
function readCommentSqlite(string $path): array
{
if (!class_exists(PDO::class)) {
return [];
}
try {
$pdo = new PDO('sqlite:' . $path);
$tables = $pdo->query("SELECT name FROM sqlite_master WHERE type='table'")->fetchAll(PDO::FETCH_COLUMN);
$records = [];
foreach ($tables as $table) {
if (!str_contains(strtolower((string) $table), 'comment')) {
continue;
}
$rows = $pdo->query('SELECT * FROM "' . str_replace('"', '""', (string) $table) . '"')->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) {
$records[] = normalizeCommentRecord($row);
}
}
return $records;
} catch (Throwable) {
return [];
}
}
/** @return array<int, array<string, mixed>> */
function collectComments(mixed $value, string $slugHint = ''): array
{
if (!is_array($value)) {
return [];
}
if (isCommentRecord($value)) {
$record = normalizeCommentRecord($value);
if (($record['slug'] ?? '') === '' && $slugHint !== '') {
$record['slug'] = $slugHint;
}
return [$record];
}
$records = [];
foreach ($value as $key => $child) {
$nextHint = is_string($key) && $key !== '' && !is_numeric($key) ? $key : $slugHint;
foreach (collectComments($child, $nextHint) as $record) {
$records[] = $record;
}
}
return $records;
}
function isCommentRecord(array $value): bool
{
$bodyKeys = ['body', 'content', 'comment', 'message', 'text'];
$identityKeys = ['author', 'name', 'email', 'username'];
return array_intersect($bodyKeys, array_keys($value)) !== []
&& array_intersect($identityKeys, array_keys($value)) !== [];
}
/** @param array<string, mixed> $record @return array<string, mixed> */
function normalizeCommentRecord(array $record): array
{
$slug = (string) (
$record['slug']
?? $record['page']
?? $record['pageKey']
?? $record['page_key']
?? $record['post']
?? $record['post_slug']
?? ''
);
return [
'slug' => $slug,
'author' => $record['author'] ?? $record['name'] ?? $record['username'] ?? 'Imported',
'email' => $record['email'] ?? '',
'website' => $record['website'] ?? $record['url'] ?? $record['site'] ?? '',
'body' => $record['body'] ?? $record['content'] ?? $record['comment'] ?? $record['message'] ?? $record['text'] ?? '',
'status' => $record['status'] ?? $record['state'] ?? 'approved',
'date' => $record['date'] ?? $record['created'] ?? $record['created_at'] ?? '',
'modified' => $record['modified'] ?? $record['updated'] ?? $record['updated_at'] ?? '',
'user_agent' => $record['user_agent'] ?? $record['agent'] ?? '',
];
}
function line(string $message): void
{
echo $message . PHP_EOL;
}
function fail(string $message): never
{
fwrite(STDERR, $message . PHP_EOL);
exit(1);
}