From 48ae8ac69c7adf923ed21b397db229799368fe5d Mon Sep 17 00:00:00 2001 From: drigato Date: Wed, 19 Nov 2014 14:10:53 -0500 Subject: [PATCH 01/13] Scheduler->removeItems optimization --- airtime_mvc/application/models/Scheduler.php | 50 +++++++------------- 1 file changed, 16 insertions(+), 34 deletions(-) diff --git a/airtime_mvc/application/models/Scheduler.php b/airtime_mvc/application/models/Scheduler.php index 39415beaa..05b55bdba 100644 --- a/airtime_mvc/application/models/Scheduler.php +++ b/airtime_mvc/application/models/Scheduler.php @@ -1112,9 +1112,12 @@ class Application_Model_Scheduler $removedItems = CcScheduleQuery::create()->findPks($scheduledIds); - //check to make sure all items selected are up to date - foreach ($removedItems as $removedItem) { + // This array is used to keep track of every show instance that was + // effected by the track deletion. It will be used later on to + // remove gaps in the schedule and adjust crossfade times. + $effectedInstanceIds = array(); + foreach ($removedItems as $removedItem) { $instance = $removedItem->getCcShowInstances($this->con); //check if instance is linked and if so get the schedule items @@ -1122,25 +1125,22 @@ class Application_Model_Scheduler if (!$cancelShow && $instance->getCcShow()->isLinked()) { //returns all linked instances if linked $ccShowInstances = $this->getInstances($instance->getDbId()); + $instanceIds = array(); foreach ($ccShowInstances as $ccShowInstance) { $instanceIds[] = $ccShowInstance->getDbId(); } - /* - * Find all the schedule items that are in the same position - * as the selected item by the user. - * The position of each track is the same across each linked instance - */ + $effectedInstanceIds = array_merge($effectedInstanceIds, $instanceIds); + + // Delete the same track, represented by $removedItem, in + // each linked show instance. $itemsToDelete = CcScheduleQuery::create() ->filterByDbPosition($removedItem->getDbPosition()) ->filterByDbInstanceId($instanceIds, Criteria::IN) - ->find(); - foreach ($itemsToDelete as $item) { - if (!$removedItems->contains($item)) { - $removedItems->append($item); - } - } + ->filterByDbId($removedItem->getDbId(), Criteria::NOT_EQUAL) + ->delete($this->con); } + //check to truncate the currently playing item instead of deleting it. if ($removedItem->isCurrentItem($this->epochNow)) { @@ -1165,29 +1165,11 @@ class Application_Model_Scheduler } else { $removedItem->delete($this->con); } - - // update is_scheduled in cc_files but only if - // the file is not scheduled somewhere else - $fileId = $removedItem->getDbFileId(); - // check if the removed item is scheduled somewhere else - $futureScheduledFiles = Application_Model_Schedule::getAllFutureScheduledFiles(); - if (!is_null($fileId) && !in_array($fileId, $futureScheduledFiles)) { - $db_file = CcFilesQuery::create()->findPk($fileId, $this->con); - $db_file->setDbIsScheduled(false)->save($this->con); - } } + Application_Model_StoredFile::updatePastFilesIsScheduled(); if ($adjustSched === true) { - //get the show instances of the shows we must adjust times for. - foreach ($removedItems as $item) { - - $instance = $item->getDBInstanceId(); - if (!in_array($instance, $showInstances)) { - $showInstances[] = $instance; - } - } - - foreach ($showInstances as $instance) { + foreach ($effectedInstanceIds as $instance) { $this->removeGaps($instance); $this->calculateCrossfades($instance); } @@ -1195,7 +1177,7 @@ class Application_Model_Scheduler //update the status flag in cc_schedule. $instances = CcShowInstancesQuery::create() - ->filterByPrimaryKeys($showInstances) + ->filterByPrimaryKeys($effectedInstanceIds) ->find($this->con); foreach ($instances as $instance) { From 00fbda193e2e0ccd6459eb5dc0a37888e4d7e4e2 Mon Sep 17 00:00:00 2001 From: drigato Date: Wed, 19 Nov 2014 17:09:54 -0500 Subject: [PATCH 02/13] Schedule->removeItems optimization fix for deleting the current playing track --- airtime_mvc/application/models/Scheduler.php | 1 + 1 file changed, 1 insertion(+) diff --git a/airtime_mvc/application/models/Scheduler.php b/airtime_mvc/application/models/Scheduler.php index 05b55bdba..4208ff5c7 100644 --- a/airtime_mvc/application/models/Scheduler.php +++ b/airtime_mvc/application/models/Scheduler.php @@ -1119,6 +1119,7 @@ class Application_Model_Scheduler foreach ($removedItems as $removedItem) { $instance = $removedItem->getCcShowInstances($this->con); + $effectedInstanceIds[] = $instance->getDbId(); //check if instance is linked and if so get the schedule items //for all linked instances so we can delete them too From 8fbe7dd6499e146616673d30256595705f26f602 Mon Sep 17 00:00:00 2001 From: Albert Santoni Date: Wed, 26 Nov 2014 17:50:59 -0500 Subject: [PATCH 03/13] Report an error and die if amazon.conf is missing --- airtime_mvc/application/configs/conf.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/airtime_mvc/application/configs/conf.php b/airtime_mvc/application/configs/conf.php index fd66453dd..38492c11e 100644 --- a/airtime_mvc/application/configs/conf.php +++ b/airtime_mvc/application/configs/conf.php @@ -27,7 +27,13 @@ class Config { // Parse separate conf file for Amazon S3 values $amazonFilename = isset($_SERVER['AMAZONS3_CONF']) ? $_SERVER['AMAZONS3_CONF'] : "/etc/airtime-saas/amazon.conf"; - $amazonValues = parse_ini_file($amazonFilename, true); + try { + $amazonValues = parse_ini_file($amazonFilename, true); + } catch (ErrorException $e) { + //This file gets loaded before the Zend bootstrap even runs so our exception handlers aren't installed yet. + //Just die with an error here then instead or handling the error any other way. + die("Error: Invalid or missing $amazonFilename."); + } $CC_CONFIG['cloud_storage'] = $amazonValues['cloud_storage']; $values = parse_ini_file($filename, true); From 670a63df87ddd616b405a22093b98c33e87cc7af Mon Sep 17 00:00:00 2001 From: Albert Santoni Date: Wed, 26 Nov 2014 18:04:46 -0500 Subject: [PATCH 04/13] Error handling for if propel isn't found / composer wasn't run --- airtime_mvc/application/Bootstrap.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/airtime_mvc/application/Bootstrap.php b/airtime_mvc/application/Bootstrap.php index 9bc36a49f..286761395 100644 --- a/airtime_mvc/application/Bootstrap.php +++ b/airtime_mvc/application/Bootstrap.php @@ -3,7 +3,10 @@ require_once __DIR__."/configs/conf.php"; $CC_CONFIG = Config::getConfig(); require_once __DIR__."/configs/ACL.php"; -require_once 'propel/propel1/runtime/lib/Propel.php'; +if (!@include_once('propel/propel1/runtime/lib/Propel.php')) +{ + die('Error: Propel not found. Did you install Airtime\'s third-party dependencies with composer? (Check the README.)'); +} Propel::init(__DIR__."/configs/airtime-conf-production.php"); From e59cd11370e94b34ad2566b13488783a99fcd231 Mon Sep 17 00:00:00 2001 From: drigato Date: Thu, 27 Nov 2014 13:48:34 -0500 Subject: [PATCH 05/13] Close the session when a track is previewed or downloaded. Close the file pointer when we are down with it. --- airtime_mvc/application/controllers/ApiController.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/airtime_mvc/application/controllers/ApiController.php b/airtime_mvc/application/controllers/ApiController.php index e6a3e982e..f8887d355 100644 --- a/airtime_mvc/application/controllers/ApiController.php +++ b/airtime_mvc/application/controllers/ApiController.php @@ -78,6 +78,10 @@ class ApiController extends Zend_Controller_Action */ public function getMediaAction() { + // Close the session so other HTTP requests can be completed while + // tracks are read for previewing or downloading. + session_write_close(); + $fileId = $this->_getParam("file"); $media = Application_Model_StoredFile::RecallById($fileId); @@ -168,10 +172,11 @@ class ApiController extends Zend_Controller_Action while (@ob_end_flush()); // NOTE: We can't use fseek here because it does not work with streams - // (a.k.a. Files stored on Amazon S3) + // (a.k.a. Files stored in the cloud) while(!feof($fm) && (connection_status() == 0)) { echo fread($fm, 1024 * 8); } + fclose($fm); } //Used by the SaaS monitoring From 6cb993cc8007c6b37ff539db7e0675bd8793694a Mon Sep 17 00:00:00 2001 From: drigato Date: Thu, 27 Nov 2014 15:44:53 -0500 Subject: [PATCH 06/13] SAAS-504: Store provider in db --- .../models/airtime/map/CloudFileTableMap.php | 1 + .../models/airtime/om/BaseCloudFile.php | 74 ++++++++++++++++--- .../models/airtime/om/BaseCloudFilePeer.php | 33 +++++---- .../models/airtime/om/BaseCloudFileQuery.php | 35 ++++++++- .../rest/controllers/MediaController.php | 3 +- airtime_mvc/build/schema.xml | 1 + airtime_mvc/build/sql/schema.sql | 4 +- 7 files changed, 123 insertions(+), 28 deletions(-) diff --git a/airtime_mvc/application/models/airtime/map/CloudFileTableMap.php b/airtime_mvc/application/models/airtime/map/CloudFileTableMap.php index 898fc0b79..5f9702bf2 100644 --- a/airtime_mvc/application/models/airtime/map/CloudFileTableMap.php +++ b/airtime_mvc/application/models/airtime/map/CloudFileTableMap.php @@ -40,6 +40,7 @@ class CloudFileTableMap extends TableMap $this->setPrimaryKeyMethodInfo('cloud_file_id_seq'); // columns $this->addPrimaryKey('id', 'DbId', 'INTEGER', true, null, null); + $this->addColumn('storage_backend', 'StorageBackend', 'VARCHAR', true, 512, null); $this->addColumn('resource_id', 'ResourceId', 'LONGVARCHAR', true, null, null); $this->addForeignKey('cc_file_id', 'CcFileId', 'INTEGER', 'cc_files', 'id', false, null, null); // validators diff --git a/airtime_mvc/application/models/airtime/om/BaseCloudFile.php b/airtime_mvc/application/models/airtime/om/BaseCloudFile.php index f9b2e0727..988fadeb9 100644 --- a/airtime_mvc/application/models/airtime/om/BaseCloudFile.php +++ b/airtime_mvc/application/models/airtime/om/BaseCloudFile.php @@ -35,6 +35,12 @@ abstract class BaseCloudFile extends BaseObject implements Persistent */ protected $id; + /** + * The value for the storage_backend field. + * @var string + */ + protected $storage_backend; + /** * The value for the resource_id field. * @var string @@ -83,6 +89,17 @@ abstract class BaseCloudFile extends BaseObject implements Persistent return $this->id; } + /** + * Get the [storage_backend] column value. + * + * @return string + */ + public function getStorageBackend() + { + + return $this->storage_backend; + } + /** * Get the [resource_id] column value. * @@ -126,6 +143,27 @@ abstract class BaseCloudFile extends BaseObject implements Persistent return $this; } // setDbId() + /** + * Set the value of [storage_backend] column. + * + * @param string $v new value + * @return CloudFile The current object (for fluent API support) + */ + public function setStorageBackend($v) + { + if ($v !== null && is_numeric($v)) { + $v = (string) $v; + } + + if ($this->storage_backend !== $v) { + $this->storage_backend = $v; + $this->modifiedColumns[] = CloudFilePeer::STORAGE_BACKEND; + } + + + return $this; + } // setStorageBackend() + /** * Set the value of [resource_id] column. * @@ -205,8 +243,9 @@ abstract class BaseCloudFile extends BaseObject implements Persistent try { $this->id = ($row[$startcol + 0] !== null) ? (int) $row[$startcol + 0] : null; - $this->resource_id = ($row[$startcol + 1] !== null) ? (string) $row[$startcol + 1] : null; - $this->cc_file_id = ($row[$startcol + 2] !== null) ? (int) $row[$startcol + 2] : null; + $this->storage_backend = ($row[$startcol + 1] !== null) ? (string) $row[$startcol + 1] : null; + $this->resource_id = ($row[$startcol + 2] !== null) ? (string) $row[$startcol + 2] : null; + $this->cc_file_id = ($row[$startcol + 3] !== null) ? (int) $row[$startcol + 3] : null; $this->resetModified(); $this->setNew(false); @@ -216,7 +255,7 @@ abstract class BaseCloudFile extends BaseObject implements Persistent } $this->postHydrate($row, $startcol, $rehydrate); - return $startcol + 3; // 3 = CloudFilePeer::NUM_HYDRATE_COLUMNS. + return $startcol + 4; // 4 = CloudFilePeer::NUM_HYDRATE_COLUMNS. } catch (Exception $e) { throw new PropelException("Error populating CloudFile object", $e); @@ -457,6 +496,9 @@ abstract class BaseCloudFile extends BaseObject implements Persistent if ($this->isColumnModified(CloudFilePeer::ID)) { $modifiedColumns[':p' . $index++] = '"id"'; } + if ($this->isColumnModified(CloudFilePeer::STORAGE_BACKEND)) { + $modifiedColumns[':p' . $index++] = '"storage_backend"'; + } if ($this->isColumnModified(CloudFilePeer::RESOURCE_ID)) { $modifiedColumns[':p' . $index++] = '"resource_id"'; } @@ -477,6 +519,9 @@ abstract class BaseCloudFile extends BaseObject implements Persistent case '"id"': $stmt->bindValue($identifier, $this->id, PDO::PARAM_INT); break; + case '"storage_backend"': + $stmt->bindValue($identifier, $this->storage_backend, PDO::PARAM_STR); + break; case '"resource_id"': $stmt->bindValue($identifier, $this->resource_id, PDO::PARAM_STR); break; @@ -626,9 +671,12 @@ abstract class BaseCloudFile extends BaseObject implements Persistent return $this->getDbId(); break; case 1: - return $this->getResourceId(); + return $this->getStorageBackend(); break; case 2: + return $this->getResourceId(); + break; + case 3: return $this->getCcFileId(); break; default: @@ -661,8 +709,9 @@ abstract class BaseCloudFile extends BaseObject implements Persistent $keys = CloudFilePeer::getFieldNames($keyType); $result = array( $keys[0] => $this->getDbId(), - $keys[1] => $this->getResourceId(), - $keys[2] => $this->getCcFileId(), + $keys[1] => $this->getStorageBackend(), + $keys[2] => $this->getResourceId(), + $keys[3] => $this->getCcFileId(), ); $virtualColumns = $this->virtualColumns; foreach ($virtualColumns as $key => $virtualColumn) { @@ -711,9 +760,12 @@ abstract class BaseCloudFile extends BaseObject implements Persistent $this->setDbId($value); break; case 1: - $this->setResourceId($value); + $this->setStorageBackend($value); break; case 2: + $this->setResourceId($value); + break; + case 3: $this->setCcFileId($value); break; } // switch() @@ -741,8 +793,9 @@ abstract class BaseCloudFile extends BaseObject implements Persistent $keys = CloudFilePeer::getFieldNames($keyType); if (array_key_exists($keys[0], $arr)) $this->setDbId($arr[$keys[0]]); - if (array_key_exists($keys[1], $arr)) $this->setResourceId($arr[$keys[1]]); - if (array_key_exists($keys[2], $arr)) $this->setCcFileId($arr[$keys[2]]); + if (array_key_exists($keys[1], $arr)) $this->setStorageBackend($arr[$keys[1]]); + if (array_key_exists($keys[2], $arr)) $this->setResourceId($arr[$keys[2]]); + if (array_key_exists($keys[3], $arr)) $this->setCcFileId($arr[$keys[3]]); } /** @@ -755,6 +808,7 @@ abstract class BaseCloudFile extends BaseObject implements Persistent $criteria = new Criteria(CloudFilePeer::DATABASE_NAME); if ($this->isColumnModified(CloudFilePeer::ID)) $criteria->add(CloudFilePeer::ID, $this->id); + if ($this->isColumnModified(CloudFilePeer::STORAGE_BACKEND)) $criteria->add(CloudFilePeer::STORAGE_BACKEND, $this->storage_backend); if ($this->isColumnModified(CloudFilePeer::RESOURCE_ID)) $criteria->add(CloudFilePeer::RESOURCE_ID, $this->resource_id); if ($this->isColumnModified(CloudFilePeer::CC_FILE_ID)) $criteria->add(CloudFilePeer::CC_FILE_ID, $this->cc_file_id); @@ -820,6 +874,7 @@ abstract class BaseCloudFile extends BaseObject implements Persistent */ public function copyInto($copyObj, $deepCopy = false, $makeNew = true) { + $copyObj->setStorageBackend($this->getStorageBackend()); $copyObj->setResourceId($this->getResourceId()); $copyObj->setCcFileId($this->getCcFileId()); @@ -938,6 +993,7 @@ abstract class BaseCloudFile extends BaseObject implements Persistent public function clear() { $this->id = null; + $this->storage_backend = null; $this->resource_id = null; $this->cc_file_id = null; $this->alreadyInSave = false; diff --git a/airtime_mvc/application/models/airtime/om/BaseCloudFilePeer.php b/airtime_mvc/application/models/airtime/om/BaseCloudFilePeer.php index 93a0f4d7b..4a1b641c1 100644 --- a/airtime_mvc/application/models/airtime/om/BaseCloudFilePeer.php +++ b/airtime_mvc/application/models/airtime/om/BaseCloudFilePeer.php @@ -24,17 +24,20 @@ abstract class BaseCloudFilePeer const TM_CLASS = 'CloudFileTableMap'; /** The total number of columns. */ - const NUM_COLUMNS = 3; + const NUM_COLUMNS = 4; /** The number of lazy-loaded columns. */ const NUM_LAZY_LOAD_COLUMNS = 0; /** The number of columns to hydrate (NUM_COLUMNS - NUM_LAZY_LOAD_COLUMNS) */ - const NUM_HYDRATE_COLUMNS = 3; + const NUM_HYDRATE_COLUMNS = 4; /** the column name for the id field */ const ID = 'cloud_file.id'; + /** the column name for the storage_backend field */ + const STORAGE_BACKEND = 'cloud_file.storage_backend'; + /** the column name for the resource_id field */ const RESOURCE_ID = 'cloud_file.resource_id'; @@ -60,12 +63,12 @@ abstract class BaseCloudFilePeer * e.g. CloudFilePeer::$fieldNames[CloudFilePeer::TYPE_PHPNAME][0] = 'Id' */ protected static $fieldNames = array ( - BasePeer::TYPE_PHPNAME => array ('DbId', 'ResourceId', 'CcFileId', ), - BasePeer::TYPE_STUDLYPHPNAME => array ('dbId', 'resourceId', 'ccFileId', ), - BasePeer::TYPE_COLNAME => array (CloudFilePeer::ID, CloudFilePeer::RESOURCE_ID, CloudFilePeer::CC_FILE_ID, ), - BasePeer::TYPE_RAW_COLNAME => array ('ID', 'RESOURCE_ID', 'CC_FILE_ID', ), - BasePeer::TYPE_FIELDNAME => array ('id', 'resource_id', 'cc_file_id', ), - BasePeer::TYPE_NUM => array (0, 1, 2, ) + BasePeer::TYPE_PHPNAME => array ('DbId', 'StorageBackend', 'ResourceId', 'CcFileId', ), + BasePeer::TYPE_STUDLYPHPNAME => array ('dbId', 'storageBackend', 'resourceId', 'ccFileId', ), + BasePeer::TYPE_COLNAME => array (CloudFilePeer::ID, CloudFilePeer::STORAGE_BACKEND, CloudFilePeer::RESOURCE_ID, CloudFilePeer::CC_FILE_ID, ), + BasePeer::TYPE_RAW_COLNAME => array ('ID', 'STORAGE_BACKEND', 'RESOURCE_ID', 'CC_FILE_ID', ), + BasePeer::TYPE_FIELDNAME => array ('id', 'storage_backend', 'resource_id', 'cc_file_id', ), + BasePeer::TYPE_NUM => array (0, 1, 2, 3, ) ); /** @@ -75,12 +78,12 @@ abstract class BaseCloudFilePeer * e.g. CloudFilePeer::$fieldNames[BasePeer::TYPE_PHPNAME]['Id'] = 0 */ protected static $fieldKeys = array ( - BasePeer::TYPE_PHPNAME => array ('DbId' => 0, 'ResourceId' => 1, 'CcFileId' => 2, ), - BasePeer::TYPE_STUDLYPHPNAME => array ('dbId' => 0, 'resourceId' => 1, 'ccFileId' => 2, ), - BasePeer::TYPE_COLNAME => array (CloudFilePeer::ID => 0, CloudFilePeer::RESOURCE_ID => 1, CloudFilePeer::CC_FILE_ID => 2, ), - BasePeer::TYPE_RAW_COLNAME => array ('ID' => 0, 'RESOURCE_ID' => 1, 'CC_FILE_ID' => 2, ), - BasePeer::TYPE_FIELDNAME => array ('id' => 0, 'resource_id' => 1, 'cc_file_id' => 2, ), - BasePeer::TYPE_NUM => array (0, 1, 2, ) + BasePeer::TYPE_PHPNAME => array ('DbId' => 0, 'StorageBackend' => 1, 'ResourceId' => 2, 'CcFileId' => 3, ), + BasePeer::TYPE_STUDLYPHPNAME => array ('dbId' => 0, 'storageBackend' => 1, 'resourceId' => 2, 'ccFileId' => 3, ), + BasePeer::TYPE_COLNAME => array (CloudFilePeer::ID => 0, CloudFilePeer::STORAGE_BACKEND => 1, CloudFilePeer::RESOURCE_ID => 2, CloudFilePeer::CC_FILE_ID => 3, ), + BasePeer::TYPE_RAW_COLNAME => array ('ID' => 0, 'STORAGE_BACKEND' => 1, 'RESOURCE_ID' => 2, 'CC_FILE_ID' => 3, ), + BasePeer::TYPE_FIELDNAME => array ('id' => 0, 'storage_backend' => 1, 'resource_id' => 2, 'cc_file_id' => 3, ), + BasePeer::TYPE_NUM => array (0, 1, 2, 3, ) ); /** @@ -155,10 +158,12 @@ abstract class BaseCloudFilePeer { if (null === $alias) { $criteria->addSelectColumn(CloudFilePeer::ID); + $criteria->addSelectColumn(CloudFilePeer::STORAGE_BACKEND); $criteria->addSelectColumn(CloudFilePeer::RESOURCE_ID); $criteria->addSelectColumn(CloudFilePeer::CC_FILE_ID); } else { $criteria->addSelectColumn($alias . '.id'); + $criteria->addSelectColumn($alias . '.storage_backend'); $criteria->addSelectColumn($alias . '.resource_id'); $criteria->addSelectColumn($alias . '.cc_file_id'); } diff --git a/airtime_mvc/application/models/airtime/om/BaseCloudFileQuery.php b/airtime_mvc/application/models/airtime/om/BaseCloudFileQuery.php index d09041387..e57ff2797 100644 --- a/airtime_mvc/application/models/airtime/om/BaseCloudFileQuery.php +++ b/airtime_mvc/application/models/airtime/om/BaseCloudFileQuery.php @@ -7,10 +7,12 @@ * * * @method CloudFileQuery orderByDbId($order = Criteria::ASC) Order by the id column + * @method CloudFileQuery orderByStorageBackend($order = Criteria::ASC) Order by the storage_backend column * @method CloudFileQuery orderByResourceId($order = Criteria::ASC) Order by the resource_id column * @method CloudFileQuery orderByCcFileId($order = Criteria::ASC) Order by the cc_file_id column * * @method CloudFileQuery groupByDbId() Group by the id column + * @method CloudFileQuery groupByStorageBackend() Group by the storage_backend column * @method CloudFileQuery groupByResourceId() Group by the resource_id column * @method CloudFileQuery groupByCcFileId() Group by the cc_file_id column * @@ -25,10 +27,12 @@ * @method CloudFile findOne(PropelPDO $con = null) Return the first CloudFile matching the query * @method CloudFile findOneOrCreate(PropelPDO $con = null) Return the first CloudFile matching the query, or a new CloudFile object populated from the query conditions when no match is found * + * @method CloudFile findOneByStorageBackend(string $storage_backend) Return the first CloudFile filtered by the storage_backend column * @method CloudFile findOneByResourceId(string $resource_id) Return the first CloudFile filtered by the resource_id column * @method CloudFile findOneByCcFileId(int $cc_file_id) Return the first CloudFile filtered by the cc_file_id column * * @method array findByDbId(int $id) Return CloudFile objects filtered by the id column + * @method array findByStorageBackend(string $storage_backend) Return CloudFile objects filtered by the storage_backend column * @method array findByResourceId(string $resource_id) Return CloudFile objects filtered by the resource_id column * @method array findByCcFileId(int $cc_file_id) Return CloudFile objects filtered by the cc_file_id column * @@ -138,7 +142,7 @@ abstract class BaseCloudFileQuery extends ModelCriteria */ protected function findPkSimple($key, $con) { - $sql = 'SELECT "id", "resource_id", "cc_file_id" FROM "cloud_file" WHERE "id" = :p0'; + $sql = 'SELECT "id", "storage_backend", "resource_id", "cc_file_id" FROM "cloud_file" WHERE "id" = :p0'; try { $stmt = $con->prepare($sql); $stmt->bindValue(':p0', $key, PDO::PARAM_INT); @@ -269,6 +273,35 @@ abstract class BaseCloudFileQuery extends ModelCriteria return $this->addUsingAlias(CloudFilePeer::ID, $dbId, $comparison); } + /** + * Filter the query on the storage_backend column + * + * Example usage: + * + * $query->filterByStorageBackend('fooValue'); // WHERE storage_backend = 'fooValue' + * $query->filterByStorageBackend('%fooValue%'); // WHERE storage_backend LIKE '%fooValue%' + * + * + * @param string $storageBackend The value to use as filter. + * Accepts wildcards (* and % trigger a LIKE) + * @param string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL + * + * @return CloudFileQuery The current query, for fluid interface + */ + public function filterByStorageBackend($storageBackend = null, $comparison = null) + { + if (null === $comparison) { + if (is_array($storageBackend)) { + $comparison = Criteria::IN; + } elseif (preg_match('/[\%\*]/', $storageBackend)) { + $storageBackend = str_replace('*', '%', $storageBackend); + $comparison = Criteria::LIKE; + } + } + + return $this->addUsingAlias(CloudFilePeer::STORAGE_BACKEND, $storageBackend, $comparison); + } + /** * Filter the query on the resource_id column * diff --git a/airtime_mvc/application/modules/rest/controllers/MediaController.php b/airtime_mvc/application/modules/rest/controllers/MediaController.php index 43550cdde..6563e857a 100644 --- a/airtime_mvc/application/modules/rest/controllers/MediaController.php +++ b/airtime_mvc/application/modules/rest/controllers/MediaController.php @@ -180,7 +180,7 @@ class Rest_MediaController extends Zend_Rest_Controller $file = CcFilesQuery::create()->findPk($id); // Since we check for this value when deleting files, set it first - $file->setDbDirectory(self::MUSIC_DIRS_STOR_PK); + //$file->setDbDirectory(self::MUSIC_DIRS_STOR_PK); $requestData = json_decode($this->getRequest()->getRawBody(), true); $whiteList = $this->removeBlacklistedFieldsFromRequestData($requestData); @@ -203,6 +203,7 @@ class Rest_MediaController extends Zend_Rest_Controller return; } $cloudFile = new CloudFile(); + $cloudFile->setStorageBackend($requestData["storage_backend"]); $cloudFile->setResourceId($requestData["resource_id"]); $cloudFile->setCcFiles($file); $cloudFile->save(); diff --git a/airtime_mvc/build/schema.xml b/airtime_mvc/build/schema.xml index ff76e268e..d8cabfeb1 100644 --- a/airtime_mvc/build/schema.xml +++ b/airtime_mvc/build/schema.xml @@ -101,6 +101,7 @@ + diff --git a/airtime_mvc/build/sql/schema.sql b/airtime_mvc/build/sql/schema.sql index 3c34da910..1efbc0768 100644 --- a/airtime_mvc/build/sql/schema.sql +++ b/airtime_mvc/build/sql/schema.sql @@ -110,6 +110,7 @@ DROP TABLE IF EXISTS "cloud_file" CASCADE; CREATE TABLE "cloud_file" ( "id" serial NOT NULL, + "storage_backend" VARCHAR(512) NOT NULL, "resource_id" TEXT NOT NULL, "cc_file_id" INTEGER, PRIMARY KEY ("id") @@ -603,8 +604,6 @@ CREATE TABLE "cc_listener_count" PRIMARY KEY ("id") ); ------------------------------------------------------------------------ --- cc_locale ----------------------------------------------------------------------- DROP TABLE IF EXISTS "cc_locale" CASCADE; @@ -617,7 +616,6 @@ CREATE TABLE "cc_locale" PRIMARY KEY ("id") ); ------------------------------------------------------------------------ -- cc_playout_history ----------------------------------------------------------------------- From 8601452c7158b8b1bb8869e4aa627e213c7164ac Mon Sep 17 00:00:00 2001 From: drigato Date: Thu, 27 Nov 2014 15:46:39 -0500 Subject: [PATCH 07/13] Merge conflict --- airtime_mvc/application/configs/conf.php | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/airtime_mvc/application/configs/conf.php b/airtime_mvc/application/configs/conf.php index 38492c11e..00eaecdbe 100644 --- a/airtime_mvc/application/configs/conf.php +++ b/airtime_mvc/application/configs/conf.php @@ -25,16 +25,11 @@ class Config { $filename = isset($_SERVER['AIRTIME_CONF']) ? $_SERVER['AIRTIME_CONF'] : "/etc/airtime/airtime.conf"; } - // Parse separate conf file for Amazon S3 values - $amazonFilename = isset($_SERVER['AMAZONS3_CONF']) ? $_SERVER['AMAZONS3_CONF'] : "/etc/airtime-saas/amazon.conf"; - try { - $amazonValues = parse_ini_file($amazonFilename, true); - } catch (ErrorException $e) { - //This file gets loaded before the Zend bootstrap even runs so our exception handlers aren't installed yet. - //Just die with an error here then instead or handling the error any other way. - die("Error: Invalid or missing $amazonFilename."); - } - $CC_CONFIG['cloud_storage'] = $amazonValues['cloud_storage']; + // Parse separate conf file for cloud storage values + $cloudStorageConfig = isset($_SERVER['CLOUD_STORAGE_CONF']) ? $_SERVER['CLOUD_STORAGE_CONF'] : "/etc/airtime-saas/cloud_storage.conf"; + $cloudStorageValues = parse_ini_file($cloudStorageConfig, true); + $currentStorageBackend = $cloudStorageValues['current_backend']['storage_backend']; + $CC_CONFIG['storage_backend'] = $cloudStorageValues[$currentStorageBackend]; $values = parse_ini_file($filename, true); From 92feacd46f9599f8699a879278a4a03458df27de Mon Sep 17 00:00:00 2001 From: drigato Date: Thu, 27 Nov 2014 15:50:40 -0500 Subject: [PATCH 08/13] SAAS-501: Re-jig cloud_storage.conf --- airtime_mvc/application/amazon/Amazon_S3.php | 6 +++--- .../airtime_analyzer/cloud_storage_uploader.py | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/airtime_mvc/application/amazon/Amazon_S3.php b/airtime_mvc/application/amazon/Amazon_S3.php index ea8a3e055..ce1615b1e 100644 --- a/airtime_mvc/application/amazon/Amazon_S3.php +++ b/airtime_mvc/application/amazon/Amazon_S3.php @@ -26,9 +26,9 @@ class Amazon_S3 { $CC_CONFIG = Config::getConfig(); - $this->setBucket($CC_CONFIG['cloud_storage']['bucket']); - $this->setAccessKey($CC_CONFIG['cloud_storage']['api_key']); - $this->setSecretKey($CC_CONFIG['cloud_storage']['api_key_secret']); + $this->setBucket($CC_CONFIG['storage_backend']['bucket']); + $this->setAccessKey($CC_CONFIG['storage_backend']['api_key']); + $this->setSecretKey($CC_CONFIG['storage_backend']['api_key_secret']); $this->zendServiceAmazonS3 = new Zend_Service_Amazon_S3( $this->getAccessKey(), diff --git a/python_apps/airtime_analyzer/airtime_analyzer/cloud_storage_uploader.py b/python_apps/airtime_analyzer/airtime_analyzer/cloud_storage_uploader.py index b52ac0113..6ecb2a06d 100644 --- a/python_apps/airtime_analyzer/airtime_analyzer/cloud_storage_uploader.py +++ b/python_apps/airtime_analyzer/airtime_analyzer/cloud_storage_uploader.py @@ -6,7 +6,7 @@ from libcloud.storage.providers import get_driver from libcloud.storage.types import Provider, ContainerDoesNotExistError, ObjectDoesNotExistError -CONFIG_PATH = '/etc/airtime-saas/amazon.conf' +CONFIG_PATH = '/etc/airtime-saas/cloud_storage.conf' class CloudStorageUploader: """ A class that uses Apache Libcloud's Storage API to upload objects into @@ -27,7 +27,8 @@ class CloudStorageUploader: def __init__(self): config = aa.AirtimeAnalyzerServer.read_config_file(CONFIG_PATH) - CLOUD_STORAGE_CONFIG_SECTION = "cloud_storage" + CLOUD_STORAGE_CONFIG_SECTION = config.get("current_backend", "storage_backend") + self._storage_backend = CLOUD_STORAGE_CONFIG_SECTION self._provider = config.get(CLOUD_STORAGE_CONFIG_SECTION, 'provider') self._bucket = config.get(CLOUD_STORAGE_CONFIG_SECTION, 'bucket') self._api_key = config.get(CLOUD_STORAGE_CONFIG_SECTION, 'api_key') @@ -90,5 +91,6 @@ class CloudStorageUploader: metadata["filename"] = file_base_name metadata["resource_id"] = object_name + metadata["storage_backend"] = self._storage_backend return metadata From 432245b18eb4d14b990b346dda19e5959dcb820d Mon Sep 17 00:00:00 2001 From: drigato Date: Thu, 27 Nov 2014 16:54:22 -0500 Subject: [PATCH 09/13] SAAS-502: Analyzer -> Set the station id and domain in the cloud object's metadata Set the domain name in the cloud object's metadata --- airtime_mvc/application/models/RabbitMq.php | 5 +++++ .../airtime_analyzer/airtime_analyzer/analyzer_pipeline.py | 4 +++- .../airtime_analyzer/cloud_storage_uploader.py | 3 ++- .../airtime_analyzer/airtime_analyzer/message_listener.py | 7 ++++--- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/airtime_mvc/application/models/RabbitMq.php b/airtime_mvc/application/models/RabbitMq.php index 15ad912e0..a9f699841 100644 --- a/airtime_mvc/application/models/RabbitMq.php +++ b/airtime_mvc/application/models/RabbitMq.php @@ -89,6 +89,11 @@ class Application_Model_RabbitMq $data['original_filename'] = $originalFilename; $data['callback_url'] = $callbackUrl; $data['api_key'] = $apiKey; + // Pass station name to the analyzer so we can set it with the file's metadata + // before uploading it to the cloud. This isn't a requirement for cloud storage, + // but put there as a safeguard, since all Airtime Pro stations will share the + // same bucket. + $data['station_domain'] = $stationDomain = Application_Model_Preference::GetStationName(); $jsonData = json_encode($data); self::sendMessage($exchange, 'topic', false, $jsonData, 'airtime-uploads'); diff --git a/python_apps/airtime_analyzer/airtime_analyzer/analyzer_pipeline.py b/python_apps/airtime_analyzer/airtime_analyzer/analyzer_pipeline.py index f17b1711d..39ab70400 100644 --- a/python_apps/airtime_analyzer/airtime_analyzer/analyzer_pipeline.py +++ b/python_apps/airtime_analyzer/airtime_analyzer/analyzer_pipeline.py @@ -18,7 +18,7 @@ class AnalyzerPipeline: """ @staticmethod - def run_analysis(queue, audio_file_path, import_directory, original_filename): + def run_analysis(queue, audio_file_path, import_directory, original_filename, station_domain): """Analyze and import an audio file, and put all extracted metadata into queue. Keyword arguments: @@ -31,6 +31,7 @@ class AnalyzerPipeline: preserve. The file at audio_file_path typically has a temporary randomly generated name, which is why we want to know what the original name was. + station_domain: The Airtime Pro account's domain name. i.e. bananas """ # It is super critical to initialize a separate log file here so that we # don't inherit logging/locks from the parent process. Supposedly @@ -52,6 +53,7 @@ class AnalyzerPipeline: # First, we extract the ID3 tags and other metadata: metadata = dict() metadata = MetadataAnalyzer.analyze(audio_file_path, metadata) + metadata["station_domain"] = station_domain #metadata = FileMoverAnalyzer.move(audio_file_path, import_directory, original_filename, metadata) csu = CloudStorageUploader() metadata = csu.upload_obj(audio_file_path, metadata) diff --git a/python_apps/airtime_analyzer/airtime_analyzer/cloud_storage_uploader.py b/python_apps/airtime_analyzer/airtime_analyzer/cloud_storage_uploader.py index 6ecb2a06d..d060f3bcc 100644 --- a/python_apps/airtime_analyzer/airtime_analyzer/cloud_storage_uploader.py +++ b/python_apps/airtime_analyzer/airtime_analyzer/cloud_storage_uploader.py @@ -71,7 +71,8 @@ class CloudStorageUploader: except ContainerDoesNotExistError: container = driver.create_container(self._bucket) - extra = {'meta_data': {'filename': file_base_name}} + extra = {'meta_data': {'filename': file_base_name, + 'station_domain': metadata["station_domain"]}} obj = driver.upload_object(file_path=audio_file_path, container=container, diff --git a/python_apps/airtime_analyzer/airtime_analyzer/message_listener.py b/python_apps/airtime_analyzer/airtime_analyzer/message_listener.py index f106258e1..b61c2133e 100644 --- a/python_apps/airtime_analyzer/airtime_analyzer/message_listener.py +++ b/python_apps/airtime_analyzer/airtime_analyzer/message_listener.py @@ -161,12 +161,13 @@ class MessageListener: msg_dict = json.loads(body) api_key = msg_dict["api_key"] callback_url = msg_dict["callback_url"] + station_domain = msg_dict["station_domain"] audio_file_path = msg_dict["tmp_file_path"] import_directory = msg_dict["import_directory"] original_filename = msg_dict["original_filename"] - audio_metadata = MessageListener.spawn_analyzer_process(audio_file_path, import_directory, original_filename) + audio_metadata = MessageListener.spawn_analyzer_process(audio_file_path, import_directory, original_filename, station_domain) StatusReporter.report_success_to_callback_url(callback_url, api_key, audio_metadata) except KeyError as e: @@ -205,11 +206,11 @@ class MessageListener: channel.basic_ack(delivery_tag=method_frame.delivery_tag) @staticmethod - def spawn_analyzer_process(audio_file_path, import_directory, original_filename): + def spawn_analyzer_process(audio_file_path, import_directory, original_filename, station_domain): ''' Spawn a child process to analyze and import a new audio file. ''' q = multiprocessing.Queue() p = multiprocessing.Process(target=AnalyzerPipeline.run_analysis, - args=(q, audio_file_path, import_directory, original_filename)) + args=(q, audio_file_path, import_directory, original_filename, station_domain)) p.start() p.join() if p.exitcode == 0: From 7c0a25be7f7f26c6d16324ea4d5d26f15846ee6b Mon Sep 17 00:00:00 2001 From: drigato Date: Mon, 1 Dec 2014 21:05:46 -0500 Subject: [PATCH 10/13] SAAS-505: Extract Amazon_S3 class and have it inherit from a general 'cloud backend' class --- airtime_mvc/application/amazon/Amazon_S3.php | 72 ----------------- .../application/cloud_storage/Amazon_S3.php | 74 ++++++++++++++++++ .../cloud_storage/ProxyStorageBackend.php | 44 +++++++++++ .../cloud_storage/StorageBackend.php | 55 +++++++++++++ airtime_mvc/application/configs/conf.php | 1 + .../application/models/airtime/CloudFile.php | 77 +++++-------------- airtime_mvc/public/index.php | 4 +- 7 files changed, 194 insertions(+), 133 deletions(-) delete mode 100644 airtime_mvc/application/amazon/Amazon_S3.php create mode 100644 airtime_mvc/application/cloud_storage/Amazon_S3.php create mode 100644 airtime_mvc/application/cloud_storage/ProxyStorageBackend.php create mode 100644 airtime_mvc/application/cloud_storage/StorageBackend.php diff --git a/airtime_mvc/application/amazon/Amazon_S3.php b/airtime_mvc/application/amazon/Amazon_S3.php deleted file mode 100644 index ce1615b1e..000000000 --- a/airtime_mvc/application/amazon/Amazon_S3.php +++ /dev/null @@ -1,72 +0,0 @@ -initZendServiceAmazonS3(); - } - - private function initZendServiceAmazonS3() - { - $CC_CONFIG = Config::getConfig(); - - $this->setBucket($CC_CONFIG['storage_backend']['bucket']); - $this->setAccessKey($CC_CONFIG['storage_backend']['api_key']); - $this->setSecretKey($CC_CONFIG['storage_backend']['api_key_secret']); - - $this->zendServiceAmazonS3 = new Zend_Service_Amazon_S3( - $this->getAccessKey(), - $this->getSecretKey()); - } - - public function getZendServiceAmazonS3() - { - return $this->zendServiceAmazonS3; - } - - public function getBucket() - { - return $this->bucket; - } - - private function setBucket($bucket) - { - $this->bucket = $bucket; - } - - public function getAccessKey() - { - return $this->accessKey; - } - - private function setAccessKey($accessKey) - { - $this->accessKey = $accessKey; - } - - public function getSecretKey() - { - return $this->secretKey; - } - - private function setSecretKey($secretKey) - { - $this->secretKey = $secretKey; - } -} \ No newline at end of file diff --git a/airtime_mvc/application/cloud_storage/Amazon_S3.php b/airtime_mvc/application/cloud_storage/Amazon_S3.php new file mode 100644 index 000000000..b9e405032 --- /dev/null +++ b/airtime_mvc/application/cloud_storage/Amazon_S3.php @@ -0,0 +1,74 @@ +setBucket($CC_CONFIG['storage_backend']['bucket']); + $this->setAccessKey($CC_CONFIG['storage_backend']['api_key']); + $this->setSecretKey($CC_CONFIG['storage_backend']['api_key_secret']); + + $this->zendServiceAmazonS3 = new Zend_Service_Amazon_S3( + $this->getAccessKey(), + $this->getSecretKey()); + } + + public function getAbsoluteFilePath($resourceId) + { + $endpoint = $this->zendServiceAmazonS3->getEndpoint(); + $scheme = $endpoint->getScheme(); + $host = $endpoint->getHost(); + $bucket = $this->getBucket(); + return "$scheme://$bucket.$host/".utf8_encode($resourceId); + } + + public function getSignedURL($resourceId) + { + //URL will be active for 30 minutes + $expires = time()+1800; + + $bucket = $this->getBucket(); + $secretKey = $this->getSecretKey(); + $accessKey = $this->getAccessKey(); + + $string_to_sign = utf8_encode("GET\n\n\n$expires\n/$bucket/$resourceId"); + // We need to urlencode the entire signature in case the hashed signature + // has spaces. (NOTE: utf8_encode() does not work here because it turns + // spaces into non-breaking spaces) + $signature = urlencode(base64_encode((hash_hmac("sha1", $string_to_sign, $secretKey, true)))); + + $resourceURL = $this->getAbsoluteFilePath($resourceId); + return $resourceURL."?AWSAccessKeyId=$accessKey&Expires=$expires&Signature=$signature"; + } + + public function getFileSize($resourceId) + { + $bucket = $this->getBucket(); + + $amz_resource = utf8_encode("$bucket/$resourceId"); + $amz_resource_info = $this->zendServiceAmazonS3->getInfo($amz_resource); + return $amz_resource_info["size"]; + } + + public function deletePhysicalFile($resourceId) + { + $bucket = $this->getBucket(); + $amz_resource = utf8_encode("$bucket/$resourceId"); + + if ($this->zendServiceAmazonS3->isObjectAvailable($amz_resource)) { + // removeObject() returns true even if the object was not deleted (bug?) + // so that is not a good way to do error handling. isObjectAvailable() + // does however return the correct value; We have to assume that if the + // object is available the removeObject() function will work. + $this->zendServiceAmazonS3->removeObject($amz_resource); + } else { + throw new Exception("ERROR: Could not locate object on Amazon S3"); + } + } +} diff --git a/airtime_mvc/application/cloud_storage/ProxyStorageBackend.php b/airtime_mvc/application/cloud_storage/ProxyStorageBackend.php new file mode 100644 index 000000000..db475c04a --- /dev/null +++ b/airtime_mvc/application/cloud_storage/ProxyStorageBackend.php @@ -0,0 +1,44 @@ +storageBackend = new $storageBackend(); + } + + public function getAbsoluteFilePath($resourceId) + { + return $this->storageBackend->getAbsoluteFilePath($resourceId); + } + + public function getSignedURL($resourceId) + { + return $this->storageBackend->getSignedURL($resourceId); + } + + public function getFileSize($resourceId) + { + return $this->storageBackend->getFileSize($resourceId); + } + + public function deletePhysicalFile($resourceId) + { + $this->storageBackend->deletePhysicalFile($resourceId); + } + +} diff --git a/airtime_mvc/application/cloud_storage/StorageBackend.php b/airtime_mvc/application/cloud_storage/StorageBackend.php new file mode 100644 index 000000000..84a9a8d72 --- /dev/null +++ b/airtime_mvc/application/cloud_storage/StorageBackend.php @@ -0,0 +1,55 @@ +bucket; + } + + protected function setBucket($bucket) + { + $this->bucket = $bucket; + } + + protected function getAccessKey() + { + return $this->accessKey; + } + + protected function setAccessKey($accessKey) + { + $this->accessKey = $accessKey; + } + + protected function getSecretKey() + { + return $this->secretKey; + } + + protected function setSecretKey($secretKey) + { + $this->secretKey = $secretKey; + } +} diff --git a/airtime_mvc/application/configs/conf.php b/airtime_mvc/application/configs/conf.php index 00eaecdbe..fda19171a 100644 --- a/airtime_mvc/application/configs/conf.php +++ b/airtime_mvc/application/configs/conf.php @@ -29,6 +29,7 @@ class Config { $cloudStorageConfig = isset($_SERVER['CLOUD_STORAGE_CONF']) ? $_SERVER['CLOUD_STORAGE_CONF'] : "/etc/airtime-saas/cloud_storage.conf"; $cloudStorageValues = parse_ini_file($cloudStorageConfig, true); $currentStorageBackend = $cloudStorageValues['current_backend']['storage_backend']; + $CC_CONFIG['current_backend'] = $cloudStorageValues['current_backend']['storage_backend']; $CC_CONFIG['storage_backend'] = $cloudStorageValues[$currentStorageBackend]; $values = parse_ini_file($filename, true); diff --git a/airtime_mvc/application/models/airtime/CloudFile.php b/airtime_mvc/application/models/airtime/CloudFile.php index c1d5b3b7a..099f7c4fa 100644 --- a/airtime_mvc/application/models/airtime/CloudFile.php +++ b/airtime_mvc/application/models/airtime/CloudFile.php @@ -1,13 +1,13 @@ getAbsoluteFilePath()."?".$this->getAuthenticationParams(); + if ($this->proxyStorageBackend == null) { + $this->proxyStorageBackend = new ProxyStorageBackend($this->getStorageBackend()); + } + return $this->proxyStorageBackend->getSignedURL($this->getResourceId()); } /** @@ -34,39 +38,10 @@ class CloudFile extends BaseCloudFile */ public function getAbsoluteFilePath() { - $amazon_s3 = new Amazon_S3(); - $zend_s3 = $amazon_s3->getZendServiceAmazonS3(); - $resource_id = $this->getResourceId(); - $endpoint = $zend_s3->getEndpoint(); - $scheme = $endpoint->getScheme(); - $host = $endpoint->getHost(); - $s3_bucket = $amazon_s3->getBucket(); - return "$scheme://$s3_bucket.$host/".utf8_encode($resource_id); - } - - /** - * - * Returns a string of authentication paramaters to append to the cloud - * object's URL. We need this for track preview and download because the - * objects are privately stored on Amazon S3. - */ - public function getAuthenticationParams() - { - $expires = time()+120; - $resource_id = $this->getResourceId(); - - $amazon_s3 = new Amazon_S3(); - $s3_bucket = $amazon_s3->getBucket(); - $s3_secret_key = $amazon_s3->getSecretKey(); - $s3_access_key = $amazon_s3->getAccessKey(); - - $string_to_sign = utf8_encode("GET\n\n\n$expires\n/$s3_bucket/$resource_id"); - // We need to urlencode the entire signature in case the hashed signature - // has spaces. (NOTE: utf8_encode() does not work here because it turns - // spaces into non-breaking spaces) - $signature = urlencode(base64_encode((hash_hmac("sha1", $string_to_sign, $s3_secret_key, true)))); - - return "AWSAccessKeyId=$s3_access_key&Expires=$expires&Signature=$signature"; + if ($this->proxyStorageBackend == null) { + $this->proxyStorageBackend = new ProxyStorageBackend($this->getStorageBackend()); + } + return $this->proxyStorageBackend->getAbsoluteFilePath($this->getResourceId()); } /** @@ -74,15 +49,10 @@ class CloudFile extends BaseCloudFile */ public function getFileSize() { - $amazon_s3 = new Amazon_S3(); - - $zend_s3 = $amazon_s3->getZendServiceAmazonS3(); - $bucket = $amazon_s3->getBucket(); - $resource_id = $this->getResourceId(); - - $amz_resource = utf8_encode("$bucket/$resource_id"); - $amz_resource_info = $zend_s3->getInfo($amz_resource); - return $amz_resource_info["size"]; + if ($this->proxyStorageBackend == null) { + $this->proxyStorageBackend = new ProxyStorageBackend($this->getStorageBackend()); + } + return $this->proxyStorageBackend->getFileSize($this->getResourceId()); } public function getFilename() @@ -119,21 +89,10 @@ class CloudFile extends BaseCloudFile */ public function deletePhysicalFile() { - $amazon_s3 = new Amazon_S3(); - $zend_s3 = $amazon_s3->getZendServiceAmazonS3(); - $bucket = $amazon_s3->getBucket(); - $resource_id = $this->getResourceId(); - $amz_resource = utf8_encode("$bucket/$resource_id"); - - if ($zend_s3->isObjectAvailable($amz_resource)) { - // removeObject() returns true even if the object was not deleted (bug?) - // so that is not a good way to do error handling. isObjectAvailable() - // does however return the correct value; We have to assume that if the - // object is available the removeObject() function will work. - $zend_s3->removeObject($amz_resource); - } else { - throw new Exception("ERROR: Could not locate object on Amazon S3"); + if ($this->proxyStorageBackend == null) { + $this->proxyStorageBackend = new ProxyStorageBackend($this->getStorageBackend()); } + $this->proxyStorageBackend->deletePhysicalFile($this->getResourceId()); } /** diff --git a/airtime_mvc/public/index.php b/airtime_mvc/public/index.php index a20586e82..fd1335ee3 100644 --- a/airtime_mvc/public/index.php +++ b/airtime_mvc/public/index.php @@ -57,8 +57,8 @@ if (file_exists('/usr/share/php/libzend-framework-php')) { set_include_path('/usr/share/php/libzend-framework-php' . PATH_SEPARATOR . get_include_path()); } -//amazon directory -set_include_path(APPLICATION_PATH . '/amazon' . PATH_SEPARATOR . get_include_path()); +//cloud storage directory +set_include_path(APPLICATION_PATH . '/cloud_storage' . PATH_SEPARATOR . get_include_path()); /** Zend_Application */ require_once 'Zend/Application.php'; From bf91677f91db54bc1d8c974b3b52c9ff0fe4a1c9 Mon Sep 17 00:00:00 2001 From: drigato Date: Tue, 2 Dec 2014 09:06:28 -0500 Subject: [PATCH 11/13] SAAS-505: Extract Amazon_S3 class and have it inherit from a general 'cloud backend' class Fixed reading credentials in from cloud_storage.conf --- airtime_mvc/application/cloud_storage/Amazon_S3.php | 10 ++++------ .../application/cloud_storage/ProxyStorageBackend.php | 4 +++- airtime_mvc/application/configs/conf.php | 8 +++++--- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/airtime_mvc/application/cloud_storage/Amazon_S3.php b/airtime_mvc/application/cloud_storage/Amazon_S3.php index b9e405032..8a7c3b387 100644 --- a/airtime_mvc/application/cloud_storage/Amazon_S3.php +++ b/airtime_mvc/application/cloud_storage/Amazon_S3.php @@ -6,13 +6,11 @@ class Amazon_S3 extends StorageBackend { private $zendServiceAmazonS3; - public function Amazon_S3() + public function Amazon_S3($securityCredentials) { - $CC_CONFIG = Config::getConfig(); - - $this->setBucket($CC_CONFIG['storage_backend']['bucket']); - $this->setAccessKey($CC_CONFIG['storage_backend']['api_key']); - $this->setSecretKey($CC_CONFIG['storage_backend']['api_key_secret']); + $this->setBucket($securityCredentials['bucket']); + $this->setAccessKey($securityCredentials['api_key']); + $this->setSecretKey($securityCredentials['api_key_secret']); $this->zendServiceAmazonS3 = new Zend_Service_Amazon_S3( $this->getAccessKey(), diff --git a/airtime_mvc/application/cloud_storage/ProxyStorageBackend.php b/airtime_mvc/application/cloud_storage/ProxyStorageBackend.php index db475c04a..78aeb1b35 100644 --- a/airtime_mvc/application/cloud_storage/ProxyStorageBackend.php +++ b/airtime_mvc/application/cloud_storage/ProxyStorageBackend.php @@ -18,7 +18,9 @@ class ProxyStorageBackend extends StorageBackend */ public function ProxyStorageBackend($storageBackend) { - $this->storageBackend = new $storageBackend(); + $CC_CONFIG = Config::getConfig(); + + $this->storageBackend = new $storageBackend($CC_CONFIG[$storageBackend]); } public function getAbsoluteFilePath($resourceId) diff --git a/airtime_mvc/application/configs/conf.php b/airtime_mvc/application/configs/conf.php index fda19171a..932834fe3 100644 --- a/airtime_mvc/application/configs/conf.php +++ b/airtime_mvc/application/configs/conf.php @@ -28,9 +28,11 @@ class Config { // Parse separate conf file for cloud storage values $cloudStorageConfig = isset($_SERVER['CLOUD_STORAGE_CONF']) ? $_SERVER['CLOUD_STORAGE_CONF'] : "/etc/airtime-saas/cloud_storage.conf"; $cloudStorageValues = parse_ini_file($cloudStorageConfig, true); - $currentStorageBackend = $cloudStorageValues['current_backend']['storage_backend']; - $CC_CONFIG['current_backend'] = $cloudStorageValues['current_backend']['storage_backend']; - $CC_CONFIG['storage_backend'] = $cloudStorageValues[$currentStorageBackend]; + + $supportedStorageBackends = array('amazon_S3'); + foreach ($supportedStorageBackends as $backend) { + $CC_CONFIG[$backend] = $cloudStorageValues[$backend]; + } $values = parse_ini_file($filename, true); From e1f1807f5a1eb5fea65be6738613a9ed85ca3b6a Mon Sep 17 00:00:00 2001 From: drigato Date: Tue, 2 Dec 2014 18:46:17 -0500 Subject: [PATCH 12/13] SAAS-503: PYPO -> Use the REST API to download files Removed Amazon S3 specific code --- airtime_mvc/application/models/Schedule.php | 16 +++---- .../application/models/airtime/CloudFile.php | 2 +- python_apps/pypo/pypofile.py | 45 ++++++++++++++----- 3 files changed, 43 insertions(+), 20 deletions(-) diff --git a/airtime_mvc/application/models/Schedule.php b/airtime_mvc/application/models/Schedule.php index 2483a8b54..339464906 100644 --- a/airtime_mvc/application/models/Schedule.php +++ b/airtime_mvc/application/models/Schedule.php @@ -771,9 +771,8 @@ SQL; * @param Array $item schedule info about one track * @param Integer $media_id scheduled item's cc_files id * @param String $uri path to the scheduled item's physical location - * @param String $amazonS3ResourceId scheduled item's Amazon S3 resource id, if applicable */ - private static function createFileScheduleEvent(&$data, $item, $media_id, $uri, $amazonS3ResourceId) + private static function createFileScheduleEvent(&$data, $item, $media_id, $uri, $downloadURL, $filesize) { $start = self::AirtimeTimeToPypoTime($item["start"]); $end = self::AirtimeTimeToPypoTime($item["end"]); @@ -807,11 +806,10 @@ SQL; 'end' => $end, 'show_name' => $item["show_name"], 'replay_gain' => $replay_gain, - 'independent_event' => $independent_event + 'independent_event' => $independent_event, + 'download_url' => $downloadURL, + 'filesize' => $filesize, ); - if (!is_null($amazonS3ResourceId)) { - $schedule_item["amazonS3_resource_id"] = $amazonS3ResourceId; - } if ($schedule_item['cue_in'] > $schedule_item['cue_out']) { $schedule_item['cue_in'] = $schedule_item['cue_out']; @@ -945,9 +943,11 @@ SQL; $storedFile = Application_Model_StoredFile::RecallById($media_id); $file = $storedFile->getPropelOrm(); $uri = $file->getAbsoluteFilePath(); + // TODO: fix this URL + $downloadURL = "http://localhost/rest/media/$media_id/download"; + $filesize = $file->getFileSize(); - $amazonS3ResourceId = $file->getResourceId(); - self::createFileScheduleEvent($data, $item, $media_id, $uri, $amazonS3ResourceId); + self::createFileScheduleEvent($data, $item, $media_id, $uri, $downloadURL, $filesize); } elseif (!is_null($item['stream_id'])) { //row is type "webstream" diff --git a/airtime_mvc/application/models/airtime/CloudFile.php b/airtime_mvc/application/models/airtime/CloudFile.php index 099f7c4fa..cd2d22657 100644 --- a/airtime_mvc/application/models/airtime/CloudFile.php +++ b/airtime_mvc/application/models/airtime/CloudFile.php @@ -85,7 +85,7 @@ class CloudFile extends BaseCloudFile /** * - * Deletes the file from Amazon S3 + * Deletes the file from cloud storage */ public function deletePhysicalFile() { diff --git a/python_apps/pypo/pypofile.py b/python_apps/pypo/pypofile.py index 2c14bbbf5..c6caca342 100644 --- a/python_apps/pypo/pypofile.py +++ b/python_apps/pypo/pypofile.py @@ -9,10 +9,14 @@ import shutil import os import sys import stat - +import urllib2 +import base64 +import ConfigParser from std_err_override import LogWriter +CONFIG_PATH = '/etc/airtime/airtime.conf' + # configure logging logging.config.fileConfig("logging.cfg") logger = logging.getLogger() @@ -38,11 +42,14 @@ class PypoFile(Thread): src = media_item['uri'] dst = media_item['dst'] + """ try: src_size = os.path.getsize(src) except Exception, e: self.logger.error("Could not get size of source file: %s", src) return + """ + src_size = media_item['filesize'] dst_exists = True try: @@ -68,7 +75,18 @@ class PypoFile(Thread): """ copy will overwrite dst if it already exists """ - shutil.copy(src, dst) + #shutil.copy(src, dst) + config = self.read_config_file(CONFIG_PATH) + CONFIG_SECTION = "general" + username = config.get(CONFIG_SECTION, 'api_key') + url = media_item['download_url'] + + """ + Make HTTP request here + """ + + with open(dst, "wb") as code: + code.write(file.read()) #make file world readable os.chmod(dst, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH) @@ -108,6 +126,19 @@ class PypoFile(Thread): return media_item + def read_config_file(self, config_path): + """Parse the application's config file located at config_path.""" + config = ConfigParser.SafeConfigParser() + try: + config.readfp(open(config_path)) + except IOError as e: + logging.debug("Failed to open config file at %s: %s" % (config_path, e.strerror)) + sys.exit() + except Exception: + logging.debug(e.strerror) + sys.exit() + + return config def main(self): while True: @@ -133,15 +164,7 @@ class PypoFile(Thread): media_item = self.get_highest_priority_media_item(self.media) if media_item is not None: - """ - If an object_name exists the file is stored on Amazon S3 - """ - if 'amazonS3_resource_id' in media_item: - csd = CloudStorageDownloader() - csd.download_obj(media_item['dst'], media_item['amazonS3_resource_id']) - media_item['file_ready'] = True - else: - self.copy_file(media_item) + self.copy_file(media_item) except Exception, e: import traceback top = traceback.format_exc() From 16dc28642085db46a0d725247817b28b600b41da Mon Sep 17 00:00:00 2001 From: drigato Date: Wed, 3 Dec 2014 13:22:52 -0500 Subject: [PATCH 13/13] SAAS-503: PYPO -> Use the REST API to download files --- airtime_mvc/application/models/Schedule.php | 8 +++-- .../rest/controllers/MediaController.php | 4 +-- python_apps/pypo/pypofile.py | 30 ++++++++----------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/airtime_mvc/application/models/Schedule.php b/airtime_mvc/application/models/Schedule.php index 339464906..269b70bc7 100644 --- a/airtime_mvc/application/models/Schedule.php +++ b/airtime_mvc/application/models/Schedule.php @@ -771,6 +771,9 @@ SQL; * @param Array $item schedule info about one track * @param Integer $media_id scheduled item's cc_files id * @param String $uri path to the scheduled item's physical location + * @param String $downloadURL URL PYPO makes to the REST API to download the file for playout + * @param Integer $filsize The file's file size in bytes + * */ private static function createFileScheduleEvent(&$data, $item, $media_id, $uri, $downloadURL, $filesize) { @@ -943,8 +946,9 @@ SQL; $storedFile = Application_Model_StoredFile::RecallById($media_id); $file = $storedFile->getPropelOrm(); $uri = $file->getAbsoluteFilePath(); - // TODO: fix this URL - $downloadURL = "http://localhost/rest/media/$media_id/download"; + + $baseUrl = Application_Common_OsPath::getBaseDir(); + $downloadURL = "http://".$_SERVER['HTTP_HOST'].$baseUrl."rest/media/$media_id/download"; $filesize = $file->getFileSize(); self::createFileScheduleEvent($data, $item, $media_id, $uri, $downloadURL, $filesize); diff --git a/airtime_mvc/application/modules/rest/controllers/MediaController.php b/airtime_mvc/application/modules/rest/controllers/MediaController.php index 6563e857a..8064c7f08 100644 --- a/airtime_mvc/application/modules/rest/controllers/MediaController.php +++ b/airtime_mvc/application/modules/rest/controllers/MediaController.php @@ -70,9 +70,10 @@ class Rest_MediaController extends Zend_Rest_Controller $storedFile = new Application_Model_StoredFile($file, $con); $baseUrl = Application_Common_OsPath::getBaseDir(); + $CC_CONFIG = Config::getConfig(); $this->getResponse() ->setHttpResponseCode(200) - ->appendBody($this->_redirect($storedFile->getRelativeFileUrl($baseUrl).'/download/true')); + ->appendBody($this->_redirect($storedFile->getRelativeFileUrl($baseUrl).'/download/true/api_key/'.$CC_CONFIG["apiKey"][0])); } else { $this->fileNotFoundResponse(); } @@ -307,7 +308,6 @@ class Rest_MediaController extends Zend_Rest_Controller $authHeader = $this->getRequest()->getHeader("Authorization"); $encodedRequestApiKey = substr($authHeader, strlen("Basic ")); $encodedStoredApiKey = base64_encode($CC_CONFIG["apiKey"][0] . ":"); - if ($encodedRequestApiKey === $encodedStoredApiKey) { return true; diff --git a/python_apps/pypo/pypofile.py b/python_apps/pypo/pypofile.py index c6caca342..998c7bd26 100644 --- a/python_apps/pypo/pypofile.py +++ b/python_apps/pypo/pypofile.py @@ -2,15 +2,13 @@ from threading import Thread from Queue import Empty -from cloud_storage_downloader import CloudStorageDownloader import logging import shutil import os import sys import stat -import urllib2 -import base64 +import requests import ConfigParser from std_err_override import LogWriter @@ -41,14 +39,7 @@ class PypoFile(Thread): """ src = media_item['uri'] dst = media_item['dst'] - - """ - try: - src_size = os.path.getsize(src) - except Exception, e: - self.logger.error("Could not get size of source file: %s", src) - return - """ + src_size = media_item['filesize'] dst_exists = True @@ -81,12 +72,17 @@ class PypoFile(Thread): username = config.get(CONFIG_SECTION, 'api_key') url = media_item['download_url'] - """ - Make HTTP request here - """ - - with open(dst, "wb") as code: - code.write(file.read()) + with open(dst, "wb") as handle: + response = requests.get(url, auth=requests.auth.HTTPBasicAuth(username, ''), stream=True) + + if not response.ok: + raise Exception("%s - Error occurred downloading file" % response.status_code) + + for chunk in response.iter_content(1024): + if not chunk: + break + + handle.write(chunk) #make file world readable os.chmod(dst, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH)